<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dartling]]></title><description><![CDATA[A blog about all things Dart and Flutter!]]></description><link>https://dartling.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1620854131291/FzaNXEqy7.png</url><title>Dartling</title><link>https://dartling.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 05:42:17 GMT</lastBuildDate><atom:link href="https://dartling.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a realtime Tic-tac-toe game with Flutter and Supabase]]></title><description><![CDATA[Introduction
Another Supabase Launch Week has passed, with some exciting new updates. During a previous launch week, I had participated in a hackathon by building a multiplayer quiz game with Flutter and Supabase, and I wrote about my experience in a...]]></description><link>https://dartling.dev/build-a-realtime-tic-tac-toe-game-with-flutter-and-supabase</link><guid isPermaLink="true">https://dartling.dev/build-a-realtime-tic-tac-toe-game-with-flutter-and-supabase</guid><category><![CDATA[Flutter]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 23 Apr 2024 21:41:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713904685820/b5fcfe62-1c4a-4841-986d-3580b2b113c0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Another <a target="_blank" href="https://supabase.com/ga-week">Supabase Launch Week</a> has passed, with some exciting new updates. During a previous launch week, I had participated in a hackathon by building a multiplayer quiz game with Flutter and Supabase, and I wrote about my experience in <a target="_blank" href="https://dartling.dev/building-a-multiplayer-quiz-game-with-flutter-and-supabase">a blog post</a>.</p>
<p>In this post, we will build a two-player, realtime Tic-tac-toe game. While parts of this are similar to Supaquiz, the multiplayer quiz game I built as part of the hackathon, this post is more of a how-to guide. It will also make use of Supabase's new <a target="_blank" href="https://supabase.com/blog/anonymous-sign-ins">anonymous login</a> feature, which was implemented as a "hack" in the previous multiplayer game.</p>
<p>The full source code is on <a target="_blank" href="https://github.com/dartling/supatictactoe">GitHub</a>, so I will focus on snippets highlighting the main logic, rather than sharing snippets to build this step-by-step.</p>
<h2 id="heading-set-up-flutter-and-supabase">Set up Flutter and Supabase</h2>
<p>Lets create an empty Flutter app, and then initialize Supabase in the project.</p>
<pre><code class="lang-bash">flutter create supatictactoe --empty
<span class="hljs-built_in">cd</span> supatictactoe
supabase init
</code></pre>
<p>Then we can run <code>supabase start</code> to start Supabase locally. When running, we will have the Supabase URL and anon key which we will need for our <code>secrets.dart</code> file, which is <code>.gitignore</code>'d.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// secrets.dart</span>
<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">'http://127.0.0.1:54321'</span>;
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-string">'eyJhbGc...'</span>;
</code></pre>
<p>These secrets are used to initialise the Supabase client when setting up our dependencies:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> supabase = <span class="hljs-keyword">await</span> Supabase.initialize(
    url: supabaseUrl,
    anonKey: supabaseAnonKey,
);
</code></pre>
<h2 id="heading-anonymous-login">Anonymous login</h2>
<p>This is a very simple but very useful feature of Supabase Auth, and I'm really happy it finally landed. When building Supaquiz, the anonymous login feature was quite hacky! It would create a random email and random password and log in the user. Because email verification was turned off, a user was logged in when opening the app. This meant we could not log in with the same user, but in the context of a trivia game played with friends, it wasn't that necessary.</p>
<p>The funny part about my previous anonymous hack is that the random emails I used had the <code>dartling.dev</code> domain, and ALL emails from that domain are forwarded to me. This meant that anyone who forked <a target="_blank" href="https://github.com/yallurium/supaquiz">Supaquiz</a> and tried to play, without noticing they have to disable email verification first, would cause random Supabase email verification emails to be sent to me, if they were running on Supabase dev. I wasn't expecting it, but I got quite a few of these emails!</p>
<p>Supabase now supports this out of the box. Here's how simple this is:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span> </span>{
  <span class="hljs-keyword">final</span> GoTrueClient _auth;

  AuthService(<span class="hljs-keyword">this</span>._auth);

  Future&lt;AuthResponse&gt; signIn() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">return</span> _auth.signInAnonymously();
  }
}
</code></pre>
<p>No random email/password hacks, no random email confirmation emails in my inbox... it just works!</p>
<h2 id="heading-game-codes">Game codes</h2>
<p>Tic-tac-toe is a game for 2 players. In order to share the game with someone else, we implement "game codes" which can be shared. A user can "host" a game and share their game code. As soon as another user joins a game using that code, the game starts.</p>
<p>I go about using <code>HashIds</code> to convert game IDs to easy to share game codes in the <a target="_blank" href="https://dartling.dev/building-a-multiplayer-quiz-game-with-flutter-and-supabase#heading-game-codes">Supaquiz article</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713906154268/e54f321e-1870-4c92-9b2f-69b0b9dc23a2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713906169923/fe6ccaf4-7357-4cf1-8cb5-66bc75c644ea.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-database-schema">Database schema</h2>
<p>For the database schema, we're going with a very simple approach. The board is stored as a 9-character string on a games table, defaulting to all zeroes. We can replace the zeroes with <code>1</code> or <code>2</code> depending on which player moves.</p>
<p>We also have a <code>status</code>, and we will listen to these status updates to start the game by moving to the game screen in the Flutter app, or showing the winner when completed.</p>
<p>The <code>last_played</code> is either 1 or 2 depending on which player moved last, so that we know which player is allowed to make a move. We default to 1 so that player 2 always goes first. The <code>winner</code> is a nullable integer which will be set to 1 or 2 if one of the player wins.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">type</span> game_status <span class="hljs-keyword">as</span> enum (<span class="hljs-string">'pending'</span>, <span class="hljs-string">'started'</span>, <span class="hljs-string">'complete'</span>);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> games (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">bigint</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">identity</span> primary <span class="hljs-keyword">key</span>,
  <span class="hljs-keyword">status</span> game_status <span class="hljs-keyword">default</span> <span class="hljs-string">'pending'</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  board <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'000000000'</span>,
  last_played <span class="hljs-built_in">int</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">default</span> <span class="hljs-number">1</span>,
  winner <span class="hljs-built_in">int</span>
);

<span class="hljs-comment">-- To listen to status and board updates</span>
<span class="hljs-keyword">alter</span> publication supabase_realtime <span class="hljs-keyword">add</span> <span class="hljs-keyword">table</span> public.games;
</code></pre>
<p>The last line is used to add real-time support to the games table, as we want to react to status updates and board changes in the Flutter app.</p>
<h2 id="heading-the-game-service">The game service</h2>
<p>The Supabase logic to create and update game is abstracted in a <code>GameService</code>. In the game service, we can create a new game, join a game (for the second player), and make a move in the board. We also have a method to stream the game "state", by listening to realtime updates.</p>
<h3 id="heading-new-game">New game</h3>
<pre><code class="lang-dart">Future&lt;Game&gt; newGame() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> game =
      <span class="hljs-keyword">await</span> _supabaseClient.from(<span class="hljs-string">'games'</span>).insert({}).select().single();
  <span class="hljs-keyword">final</span> gameId = game[<span class="hljs-string">'id'</span>];
  <span class="hljs-keyword">final</span> gameCode = _codeManager.toCode(gameId);
  log(<span class="hljs-string">'Created game with code <span class="hljs-subst">$gameCode</span> (ID <span class="hljs-subst">$gameId</span>)'</span>);
  <span class="hljs-keyword">return</span> Game(gameId, gameCode, <span class="hljs-number">1</span>);
}
</code></pre>
<p>New game creates a new game. Note that we don't need to pass any information to create it, all the values are set to the default ones: <code>pending</code> status and player 1 as <code>last_played</code>, and an empty <code>board</code>. We use the <code>CodeManager</code> class to convert the game ID to a game code, using <code>HashIds</code>.</p>
<h3 id="heading-join-game">Join game</h3>
<pre><code class="lang-dart">Future&lt;Game&gt; joinGame(<span class="hljs-built_in">String</span> gameCode) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> gameId = _codeManager.toId(gameCode);
  log(<span class="hljs-string">'Searching for game with code <span class="hljs-subst">$gameCode</span> (ID <span class="hljs-subst">$gameId</span>)'</span>);

  <span class="hljs-keyword">if</span> (gameId == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">throw</span> InvalidGameCodeException(<span class="hljs-string">'Invalid code'</span>);
  }

  <span class="hljs-keyword">final</span> game = <span class="hljs-keyword">await</span> _supabaseClient
      .from(<span class="hljs-string">'games'</span>)
      .select()
      .eq(<span class="hljs-string">'id'</span>, gameId)
      .maybeSingle();
  <span class="hljs-keyword">if</span> (game == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">throw</span> InvalidGameCodeException(<span class="hljs-string">'Invalid code'</span>);
  }

  <span class="hljs-keyword">final</span> status = game[<span class="hljs-string">'status'</span>];
  log(<span class="hljs-string">'Found game with status <span class="hljs-subst">$status</span>'</span>);
  <span class="hljs-keyword">if</span> (status != <span class="hljs-string">'pending'</span>) {
    <span class="hljs-keyword">throw</span> InvalidGameCodeException(<span class="hljs-string">'Game has already started'</span>);
  }

  <span class="hljs-keyword">await</span> _supabaseClient
      .from(<span class="hljs-string">'games'</span>)
      .update({<span class="hljs-string">'status'</span>: <span class="hljs-string">'started'</span>}).eq(<span class="hljs-string">'id'</span>, gameId);

  <span class="hljs-keyword">return</span> Game(gameId, gameCode, <span class="hljs-number">2</span>);
}
</code></pre>
<p>Join game accepts a game code, converts it to an ID, and looks up for the game by ID in the database. If a game is found, and the game is still pending (which means no one else joined), we update the game status to <code>started</code>. This will trigger the game to start for both players.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713906958994/de7338cf-58fa-4bf1-a36b-e78dc352b6ca.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-play-move">Play move</h3>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; playMove(Game game, GameState state, <span class="hljs-built_in">int</span> index) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">if</span> (state.lastPlayed == game.player || state.board[index] != <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span>;
  }
  state.board[index] = game.player;
  <span class="hljs-keyword">await</span> _supabaseClient.from(<span class="hljs-string">'games'</span>).update({
    <span class="hljs-string">'board'</span>: state.board.join(),
    <span class="hljs-string">'last_played'</span>: game.player,
  }).eq(<span class="hljs-string">'id'</span>, game.id);
}
</code></pre>
<p>A player can "move" to a square in the grid, only if it's their turn, and only if the square is empty. To make this change, we replace the board string at the selected index to either 1 or 2 depending on the player, and set the <code>last_played</code> field to the player.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713907092669/64cc1b45-72bd-49fb-b50f-1aac97199477.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-stream-game-state">Stream game state</h3>
<pre><code class="lang-dart">Stream&lt;GameState&gt; streamState(<span class="hljs-built_in">int</span> gameId) {
  <span class="hljs-keyword">return</span> _supabaseClient
      .from(<span class="hljs-string">'games'</span>)
      .stream(primaryKey: [<span class="hljs-string">'id'</span>])
      .eq(<span class="hljs-string">'id'</span>, gameId)
      .map((e) =&gt; toGameState(e.first));
}

GameState toGameState(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; game) {
  <span class="hljs-keyword">final</span> status = GameStatus.fromString(game[<span class="hljs-string">'status'</span>]);
  <span class="hljs-built_in">print</span>(game[<span class="hljs-string">'board'</span>]);
  <span class="hljs-keyword">final</span> board = (game[<span class="hljs-string">'board'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String</span>)
      .split(<span class="hljs-string">''</span>)
      .map((e) =&gt; <span class="hljs-built_in">int</span>.tryParse(e) ?? <span class="hljs-number">0</span>)
      .toList();
  <span class="hljs-keyword">return</span> GameState(status, board, game[<span class="hljs-string">'last_played'</span>], game[<span class="hljs-string">'winner'</span>]);
}
</code></pre>
<p>Last but not least, we stream all updates to the game by its ID, in order to show the changes as they happen.</p>
<h2 id="heading-the-game-view">The game view</h2>
<p>In Flutter, the main game screen is a <code>StreamBuilder</code> widget, which streams the game state from the <code>GameService</code> and shows the board in a 3x3 grid.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GameView</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> Game game;

  <span class="hljs-keyword">const</span> GameView({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.game});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> textStyle = TextStyle(fontSize: <span class="hljs-number">40</span>);
    <span class="hljs-keyword">final</span> colorScheme = Theme.of(context).colorScheme;

    <span class="hljs-keyword">return</span> StreamBuilder&lt;GameState&gt;(
        stream: Services.of(context).gameService.streamState(game.id),
        builder: (context, snapshot) {
          <span class="hljs-keyword">if</span> (snapshot.connectionState != ConnectionState.active ||
              snapshot.data == <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> SizedBox();
          }
          <span class="hljs-keyword">final</span> state = snapshot.data!;
          log(<span class="hljs-string">'Game status <span class="hljs-subst">${state.status}</span>'</span>);
          <span class="hljs-keyword">if</span> (state.status == GameStatus.complete) {
            <span class="hljs-keyword">return</span> Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                    state.winner != <span class="hljs-keyword">null</span>
                        ? <span class="hljs-string">'Player <span class="hljs-subst">${state.winner!}</span> wins!'</span>
                        : <span class="hljs-string">'It\'s a tie!'</span>,
                    textAlign: TextAlign.center,
                    style: textStyle.copyWith(color: colorScheme.primary)),
                Text(
                    state.winner == game.player
                        ? <span class="hljs-string">'Congratulations!'</span>
                        : <span class="hljs-string">'Better luck next time!'</span>,
                    textAlign: TextAlign.center,
                    style: textStyle),
              ],
            );
          }
          log(<span class="hljs-string">'Game state updated with board <span class="hljs-subst">${state.board}</span>'</span>);
          <span class="hljs-keyword">return</span> Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              <span class="hljs-keyword">if</span> (state.lastPlayed != game.player) Text(<span class="hljs-string">'Your turn'</span>),
              <span class="hljs-keyword">if</span> (state.lastPlayed == game.player)
                Text(<span class="hljs-string">'Waiting for Player <span class="hljs-subst">${game.secondPlayer}</span>...'</span>),
              GridView.builder(
                padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
                itemCount: <span class="hljs-number">9</span>,
                shrinkWrap: <span class="hljs-keyword">true</span>,
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: <span class="hljs-number">3</span>,
                ),
                itemBuilder: (BuildContext context, <span class="hljs-built_in">int</span> index) {
                  <span class="hljs-keyword">return</span> GestureDetector(
                    onTap: () =&gt; _onTap(context, state, index),
                    child: Container(
                      decoration: BoxDecoration(
                        border: Border.all(color: colorScheme.secondary),
                      ),
                      child: Center(
                        child: Text(
                          state.getSquareLabel(index),
                          style: textStyle.copyWith(
                            color: state.board[index] == game.player
                                ? colorScheme.primary
                                : <span class="hljs-keyword">null</span>,
                          ),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ],
          );
        });
  }

  <span class="hljs-keyword">void</span> _onTap(BuildContext context, GameState state, <span class="hljs-built_in">int</span> index) {
    Services.of(context).gameService.playMove(game, state, index);
  }
}
</code></pre>
<p>We use <code>GridView.builder</code> to build the grid, if the game status is <code>started</code> . If the game status is <code>complete</code>, we skip the grid and show a message with the winner, if it's not a tie.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713907348767/ee88e387-f1de-4d5e-be44-07e533d7cb93.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-winning">Winning</h2>
<p>Notice that the game service does not have a method to determine the winner. We could add something there for this. For example, on every move, we could write some Dart code to check the board to see if there's a winner. If there is, we can update the game status to <code>complete</code> and set the winner.</p>
<p>While doing this on the client-side is a nice, simple way to do it, there is a different, although a bit more complicated way, to do this from the database with a function and a trigger.</p>
<p>First, we write a function <code>check_winner()</code> that checks the board for a winner. If there is one, it sets the game status to complete and updates the winner. If there's no winner, it checks if there are any empty squares left. If there aren't, it ends the game by also setting the status to complete, as there can be no winner.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Function to check if there is a winner and end the game</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">function</span> check_winner()
<span class="hljs-keyword">returns</span> <span class="hljs-keyword">trigger</span> <span class="hljs-keyword">as</span> $$
<span class="hljs-keyword">begin</span>
    <span class="hljs-comment">-- check rows, columns and diagonals for a win</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">2</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">3</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">4</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">5</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">6</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">7</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">8</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">9</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">4</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">7</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">2</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">5</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">8</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">3</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">6</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">9</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">5</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">9</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">or</span>
       <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">3</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">5</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">substring</span>(new.board, <span class="hljs-number">7</span>, <span class="hljs-number">1</span>) = <span class="hljs-string">'1'</span> <span class="hljs-keyword">then</span>
        <span class="hljs-keyword">update</span> games <span class="hljs-keyword">set</span> <span class="hljs-keyword">status</span> = <span class="hljs-string">'complete'</span>, winner = <span class="hljs-number">1</span> <span class="hljs-keyword">where</span> <span class="hljs-keyword">id</span> = new.id;
        return new;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    if substring(new.board, 1, 1) = '2' and substring(new.board, 2, 1) = '2' and substring(new.board, 3, 1) = '2' or
       substring(new.board, 4, 1) = '2' and substring(new.board, 5, 1) = '2' and substring(new.board, 6, 1) = '2' or
       substring(new.board, 7, 1) = '2' and substring(new.board, 8, 1) = '2' and substring(new.board, 9, 1) = '2' or
       substring(new.board, 1, 1) = '2' and substring(new.board, 4, 1) = '2' and substring(new.board, 7, 1) = '2' or
       substring(new.board, 2, 1) = '2' and substring(new.board, 5, 1) = '2' and substring(new.board, 8, 1) = '2' or
       substring(new.board, 3, 1) = '2' and substring(new.board, 6, 1) = '2' and substring(new.board, 9, 1) = '2' or
       substring(new.board, 1, 1) = '2' and substring(new.board, 5, 1) = '2' and substring(new.board, 9, 1) = '2' or
       substring(new.board, 3, 1) = '2' and substring(new.board, 5, 1) = '2' and substring(new.board, 7, 1) = '2' then
        <span class="hljs-keyword">update</span> games <span class="hljs-keyword">set</span> <span class="hljs-keyword">status</span> = <span class="hljs-string">'complete'</span>, winner = <span class="hljs-number">2</span> <span class="hljs-keyword">where</span> <span class="hljs-keyword">id</span> = new.id;
        return new;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    <span class="hljs-comment">-- check if the board has no zeroes indicating a full board</span>
    if position('0' in new.board) = 0 then
        <span class="hljs-keyword">update</span> games <span class="hljs-keyword">set</span> <span class="hljs-keyword">status</span> = <span class="hljs-string">'complete'</span> <span class="hljs-keyword">where</span> <span class="hljs-keyword">id</span> = new.id;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    return new;
<span class="hljs-keyword">end</span>;
$$ language plpgsql;
</code></pre>
<p>We define a trigger to call this function after every update of the <code>board</code> field of each game.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Trigger check winner function after each board update</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">trigger</span> trigger_check_winner
<span class="hljs-keyword">after</span> <span class="hljs-keyword">update</span> <span class="hljs-keyword">of</span> board <span class="hljs-keyword">on</span> games
<span class="hljs-keyword">for</span> <span class="hljs-keyword">each</span> <span class="hljs-keyword">row</span>
<span class="hljs-keyword">execute</span> <span class="hljs-keyword">function</span> check_winner();
</code></pre>
<p>You could easily argue that this is way too much complexity, and doing it on the client is the easiest way. I agree. But having the backend (in this case the database) rather than the client determine the winner feels more "right".</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this post, we built a realtime Tic-tac-toe game with Flutter and Supabase. We made use of Supabase Auth's new anonymous login feature, which was released last week as part of the Supabase Launch (GA) Week.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/supatictactoe">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Passwordless login in Flutter via email OTP with Supabase Auth]]></title><description><![CDATA[Introduction
If your mobile or web app requires user management, you're going to need a way for your users to sign up. By now, developers do not try to reinvent the wheel and write their own auth solutions, and can either use self-hosted or managed a...]]></description><link>https://dartling.dev/passwordless-login-in-flutter-via-email-otp-with-supabase-auth</link><guid isPermaLink="true">https://dartling.dev/passwordless-login-in-flutter-via-email-otp-with-supabase-auth</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Auth ]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Fri, 22 Dec 2023 18:28:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1703269490175/1164d78e-e0ce-4822-b2f1-4f6dea1e47ca.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>If your mobile or web app requires user management, you're going to need a way for your users to sign up. By now, developers do not try to reinvent the wheel and write their own auth solutions, and can either use self-hosted or managed auth solutions (and preferably open-source!).</p>
<p>Traditionally, you sign up for an app using your username or email, and a password. While still widely used, password-less login is becoming increasingly popular, allowing users to sign up just by providing an email or a phone. With password-less login, users receive a one-time password (OTP) or a "magic link" that is used to sign them in.</p>
<p>In this tutorial, we will go over how you can easily set up password-less login in a Flutter app with Supabase Auth via one-time password through email. We will also discuss the benefits of the password-less approach and potential drawbacks.</p>
<h2 id="heading-why-go-password-less">Why go password-less?</h2>
<p>Password-less login can be beneficial for two main reasons. Firstly, security. If you don't have to store passwords, you don't have to worry about passwords getting leaked, or hackers attempting to guess a user's password (either by a brute force approach or social engineering).</p>
<p>Secondly, and perhaps more importantly depending on how you see it, is user convenience. Not requiring a separate sign-up flow for your users, and simply allowing them to sign up (and in) to your app by tapping on a link sent to their email or filling in a code emailed or texted to them can reduce friction when getting new users. While you could argue that signing in with a password is quicker, especially when using a password manager, the initial sign-up process can be time-consuming, and could cause potential users to skip signing up and look for another app.</p>
<h3 id="heading-why-not">Why not?</h3>
<p>Despite these benefits, there are potential drawbacks and considerations to keep in mind.</p>
<p>Recovering access to an account can be challenging if a user has lost access to their email. With passwords, you could sign in to your account and change your email. Instead, users would have to contact the app developers or go through customer support to regain access, and even then, you will need a way to verify that the user is who they say they are. However, you could still mitigate this by requiring both an email and a phone number (though this might again introduce some additional friction in the sign-up flow). You could also prompt users to add a phone number (or email if they signed up with a phone number) later, or even a backup password, as a way to better secure their account. This way, you'd still get the benefits of a quick user sign-up.</p>
<p>Another important point to consider is dependency on external services. To receive a one-time password or magic link, you will likely rely on an external service to send an email or SMS. This could mean additional costs, but also an additional point of failure; if the email service you use is down, for example, your users won't be able to log in at all. This also becomes a bit easier to deal with if you support logging in with passwords in addition to password-less.</p>
<h2 id="heading-supabase-auth">Supabase Auth</h2>
<p>One way to implement password-less login in Flutter apps is with <a target="_blank" href="https://supabase.com/docs/guides/auth">Supabase Auth</a>. <a target="_blank" href="https://supabase.com/">Supabase</a> is an open-source alternative to Firebase, providing services such as a Postgres database, instant APIs, edge functions and storage, in addition to authentication.</p>
<p>I had previously written about using Supabase with Flutter, introducing its <a target="_blank" href="https://dartling.dev/full-stack-with-flutter-and-supabase-pt-1-authentication">database offering</a>, as well as <a target="_blank" href="https://dartling.dev/full-stack-with-flutter-and-supabase-pt-2-database">password-based auth</a>. Supabase has gone a long way since then, and so has its integration with Flutter and the Dart/Flutter SDK.</p>
<p>You can use Supabase Auth in your Flutter projects on its own, without having to use any additional Supabase product.</p>
<h2 id="heading-password-less-login">Password-less login</h2>
<p>Let's get started on implementing password-less login in a new Flutter app. To start, let's create a new Flutter project.</p>
<pre><code class="lang-bash">flutter create passwordless --empty
<span class="hljs-built_in">cd</span> passwordless
</code></pre>
<p>This creates a new Flutter project called <code>passwordless</code>, in the <code>passwordless</code> directory. We use the <code>--empty</code> flag to only start with the bare minimum, otherwise we'd get a sample counter app with a lot of comments describing what everything does.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703094013664/c424e151-792c-48cc-9344-7df472aeebcf.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-setting-up-supabase-auth">Setting up Supabase Auth</h3>
<p>Getting started with Supabase is quick and easy using the <a target="_blank" href="https://supabase.com/docs/guides/cli/getting-started">Supabase CLI</a>.</p>
<pre><code class="lang-bash">supabase init
supabase start
</code></pre>
<p>The <code>init</code> command initializes Supabase in our <code>passwordless</code> directory, and <code>start</code> runs the Supabase services locally.</p>
<p>Next, let's add the Supabase client library to our Flutter app.</p>
<pre><code class="lang-bash">flutter pub add supabase_flutter
</code></pre>
<p>This updates the dependencies in the project's <code>pubspec.yaml</code>, adding the Supabase Flutter SDK's latest version:</p>
<pre><code class="lang-bash">dependencies:
  supabase_flutter: ^2.0.2
</code></pre>
<p>Now, we need to initialize Supabase before running the app. When running Supabase locally with <code>supabase start</code>, the API URL and anon key were logged in the terminal. We need these when initializing the Supabase client.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// lib/main.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:passwordless/secrets.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_flutter/supabase_flutter.dart'</span>;

Future&lt;<span class="hljs-keyword">void</span>&gt; main() <span class="hljs-keyword">async</span> {
  WidgetsFlutterBinding.ensureInitialized();

  <span class="hljs-keyword">await</span> Supabase.initialize(
    url: supabaseUrl,
    anonKey: supabaseAnonKey,
  );
  runApp(<span class="hljs-keyword">const</span> MainApp());
}
</code></pre>
<p>We don't want to commit these properties to version control, so we're adding them as constants in a file named <code>secrets.dart</code> which is included in the <code>.gitignore</code> file.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// secrets.dart</span>
<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">'http://127.0.0.1:54321'</span>;
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-string">'eyJhbGciOiJR5cCI6IkpXVCJ9.eyQwgnWNReilDMblYTn_I0'</span>;
</code></pre>
<p>In the repository, the <code>secrets_sample.dart</code> file has the same structure with empty strings. If you're cloning this repo, simply copy this file to a new filed named <code>secrets.dart</code> and fill in the properties with either the local URL and key or your live Supabase project.</p>
<h3 id="heading-one-time-password-via-email">One-time-password via email</h3>
<p>Now that everything is set up, we can start implementing the OTP via email flow. The flow is simple:</p>
<ul>
<li><p>User taps on button sign in via OTP</p>
</li>
<li><p>User is shown a new screen in which they need to submit the OTP</p>
</li>
<li><p>User inputs the OTP received via email to sign in</p>
</li>
</ul>
<p>First, we start with a main screen with just the e-mail OTP sign in option:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// main.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MainApp({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Passwordless'</span>),
        ),
        body: Center(
          child: Column(
            children: [
              SignInOptionButton(
                label: <span class="hljs-string">'Sign in with OTP (email)'</span>,
                screen: EmailOtpScreen(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignInOptionButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> SignInOptionButton({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.label,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.screen,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> label;
  <span class="hljs-keyword">final</span> Widget screen;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Padding(
      padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
      child: OutlinedButton(
        onPressed: () {
          <span class="hljs-keyword">final</span> route = MaterialPageRoute(builder: (context) =&gt; screen);
          Navigator.push(context, route);
        },
        child: Text(label),
      ),
    );
  }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703259305545/0b54d541-c1af-4bb4-a076-5f39b0699baf.png" alt class="image--center mx-auto" /></p>
<p>Next, the user needs to input their email address to receive their one-time password.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703261418345/28d80df3-93b6-41f3-8dbe-11351df062fc.png" alt class="image--center mx-auto" /></p>
<p>Above is the <code>EmailOtpScreen</code> widget in action, below the code:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// screens/email_otp_screen.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailOtpScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  EmailOtpScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-keyword">final</span> _emailController = TextEditingController();

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(
        child: Column(
          children: [
            Padding(
              padding:
                  <span class="hljs-keyword">const</span> EdgeInsets.symmetric(vertical: <span class="hljs-number">16.0</span>, horizontal: <span class="hljs-number">24.0</span>),
              child: TextField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: <span class="hljs-keyword">const</span> InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: <span class="hljs-string">'Enter your e-mail'</span>,
                ),
              ),
            ),
            OutlinedButton(
              onPressed: () {
                <span class="hljs-keyword">final</span> email = _emailController.text;
                <span class="hljs-keyword">final</span> supabase = Supabase.instance.client;
                supabase.auth.signInWithOtp(email: email);
                <span class="hljs-keyword">final</span> route = MaterialPageRoute(
                  builder: (context) =&gt; VerifyOtpScreen(email: email),
                );
                Navigator.pushReplacement(context, route);
              },
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Get OTP'</span>),
            )
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>The two lines below are all we need to send an email with the OTP to the email supplied through the text field.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> supabase = Supabase.instance.client;
supabase.auth.signInWithOtp(email: email);
</code></pre>
<p>If you're following along and you've just tried to sign in, you'll be expecting to receive an email by now... But we're running Supabase locally, so no emails are being sent. We could set up a live Supabase project and switch the secrets to point there to receive emails, but for now we're going to keep working locally. When running Supabase locally, we can still test the email flows thanks to <a target="_blank" href="https://inbucket.org/">Inbucket</a>, a tool which lets you receive outgoing emails through a web interface. When running <code>supabase start</code>, along with the Supabase URL and anon key we used to initialize the client, the Inbucket URL was also logged. If we follow that URL, we'll see the contents of the email that would have been sent if we were working with a live Supabase project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703262117213/1b3be72f-781e-4016-8809-2ca0af30c421.png" alt class="image--center mx-auto" /></p>
<p>You'll notice the email mentions "magic link" and includes both a log in link (the magic link), as well as the code. This is due to the email template, which we can customize. Currently, it doesn't look like we can customize the template when running Supabase locally, but we can do so in a deployed Supabase project. If your app does not support magic links, you can remove the magic link from the email template and send the OTP only.</p>
<p>After tapping on "Get OTP", we redirected to a new screen to verify the OTP. Let's put in the code from the email:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703262965608/baae463a-df82-4f66-9dfc-bc88463b0543.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-dart"><span class="hljs-comment">// screens/verify_otp_screen.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VerifyOtpScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  VerifyOtpScreen({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email});

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> _otpController = TextEditingController();

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(
        child: Column(
          children: [
            Padding(
              padding:
                  <span class="hljs-keyword">const</span> EdgeInsets.symmetric(vertical: <span class="hljs-number">16.0</span>, horizontal: <span class="hljs-number">24.0</span>),
              child: TextField(
                controller: _otpController,
                keyboardType: TextInputType.number,
                decoration: <span class="hljs-keyword">const</span> InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: <span class="hljs-string">'Enter the 6-digit code'</span>,
                ),
              ),
            ),
            OutlinedButton(
              onPressed: () <span class="hljs-keyword">async</span> {
                <span class="hljs-keyword">final</span> navigator = Navigator.of(context);
                <span class="hljs-keyword">final</span> scaffoldMessenger = ScaffoldMessenger.of(context);
                <span class="hljs-keyword">try</span> {
                  <span class="hljs-keyword">final</span> response =
                      <span class="hljs-keyword">await</span> Supabase.instance.client.auth.verifyOTP(
                    email: email,
                    token: _otpController.text,
                    type: OtpType.email,
                  );
                  <span class="hljs-keyword">final</span> route = MaterialPageRoute(
                      builder: (_) =&gt; SuccessScreen(user: response.user!));
                  navigator.pushReplacement(route);
                } <span class="hljs-keyword">catch</span> (err) {
                  scaffoldMessenger.showSnackBar(
                      <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Something went wrong'</span>)));
                }
              },
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Verify OTP'</span>),
            )
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>To verify the OTP, we need to pass the email passed from the previous screen, as well as the token/code. We also need to specify the OTP type, which for this case is <code>email</code>.</p>
<p>Let's go over the Supabase code to see what's happening:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// keep references to both the navigator and scaffold messenger </span>
<span class="hljs-comment">// as the verifyOTP method is async and we should not use the </span>
<span class="hljs-comment">// context across async gaps</span>
<span class="hljs-keyword">final</span> navigator = Navigator.of(context);
<span class="hljs-keyword">final</span> scaffoldMessenger = ScaffoldMessenger.of(context);
<span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// pass the email from previous screen, OTP from the text field</span>
  <span class="hljs-keyword">final</span> response =
      <span class="hljs-keyword">await</span> Supabase.instance.client.auth.verifyOTP(
    email: email,
    token: _otpController.text,
    type: OtpType.email,
  );
  <span class="hljs-comment">// if a response is received with no error thrown, we assume</span>
  <span class="hljs-comment">// the response contains a valid user, so we show a success screen</span>
  <span class="hljs-keyword">final</span> route = MaterialPageRoute(
      builder: (_) =&gt; SuccessScreen(user: response.user!));
  navigator.pushReplacement(route);
} <span class="hljs-keyword">catch</span> (err) {
  <span class="hljs-comment">// if an error is thrown, for example if the OTP is wrong, we show</span>
  <span class="hljs-comment">// a generic error (but ideally we explain what actually went wrong)</span>
  scaffoldMessenger.showSnackBar(
      <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Something went wrong'</span>)));
}
</code></pre>
<p>If the OTP is correct, we should now be redirected to the success screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703264493433/1c7756b4-73db-4f60-8e46-925d7e678347.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-dart"><span class="hljs-comment">// screens/success_screen.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SuccessScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> SuccessScreen({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.user});

  <span class="hljs-keyword">final</span> User user;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(
        child: Column(
          children: [
            <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'You are now logged in!'</span>),
            Text(<span class="hljs-string">'Email: <span class="hljs-subst">${user.email ?? <span class="hljs-string">'--'</span>}</span>'</span>),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>We've successfully signed in to our app! Since this is the first sign-in, we are also automatically signed up.</p>
<p>For better security, as discussed previously, we could require users to sign up with an email and password, and only allow logins via OTP later. In this case, we would need to set the <code>shouldCreateUser</code> flag to <code>false</code> for the login via OTP.</p>
<pre><code class="lang-dart">Supabase.instance.client.auth.signInWithOtp(
  email: email,
  shouldCreateUser: <span class="hljs-keyword">false</span>,
);
</code></pre>
<h3 id="heading-updating-the-email-template">Updating the email template</h3>
<p>Previously, we saw that the email we received (through Inbucket) contained a magic link as well. We haven't added support for magic links in this project, so we should update the email template to stop sending them.</p>
<p>To do this, we first had to set up a live Supabase project, and change the settings to point to that instead of the local Supabase services. From the project dashboard, we can edit the email templates from the Auth tab.</p>
<p>This was the template:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Magic Link<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Follow this link to login:<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ .ConfirmationURL }}"</span>&gt;</span>Log In<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Alternatively, enter the code: {{ .Token }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>And this is how we change it to look to avoid including a magic link:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Your one-time password<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is your one-time password to sign in to Passwordless: {{ .Token }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we showed how we can easily support password-less login in Flutter via email OTP with Supabase. You can find the source code <a target="_blank" href="https://github.com/dartling/passwordless">here</a>.</p>
<p>Supabase Auth also supports password-less login via one-time password through phone, as well as magic links. We've only focused on the email OTP flow in this tutorial, but we could cover these other two approaches in the future. If you're interested, please let me know in the comments!</p>
]]></content:encoded></item><item><title><![CDATA[Create your own ChatGPT in Dart with Supabase Vector and OpenAI]]></title><description><![CDATA[Introduction
ChatGPT and AI-powered tools have been increasingly popular these days. Most recently, a lot of websites, apps and services have integrated ChatGPT or ChatGPT-like tools so you can "chat" with their documentation and content, most notabl...]]></description><link>https://dartling.dev/create-your-own-chatgpt-in-dart-with-supabase-vector-and-openai</link><guid isPermaLink="true">https://dartling.dev/create-your-own-chatgpt-in-dart-with-supabase-vector-and-openai</guid><category><![CDATA[Dart]]></category><category><![CDATA[AI]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 10 Oct 2023 22:35:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696973780186/2e13d881-ed60-4938-9e29-10ae02356f81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>ChatGPT and AI-powered tools have been increasingly popular these days. Most recently, a lot of websites, apps and services have integrated ChatGPT or ChatGPT-like tools so you can "chat" with their documentation and content, most notably <a target="_blank" href="https://supabase.com/blog/chatgpt-supabase-docs">Supabase Clippy</a>.</p>
<p>In this tutorial, we will build our own ChatGPT, with which we'll be able to store any documents in text format, and ask it questions about these documents. And while Python and JavaScript/TypeScript have been the most popular choices for such tools, we'll be doing this in Dart!</p>
<p>Why Dart? Well, this is a Dart and Flutter blog after all. For a Flutter app with AI capabilities, the AI backend could be written in any language. But why not Dart? It makes things easier as the full stack is written in the same language, and models such as requests and responses can be reused. And while some could argue that the Dart libraries are not as advanced, for this ChatGPT clone we only really need two things: <a target="_blank" href="https://supabase.com/docs/guides/ai/vector-columns">Supabase Vector</a> and OpenAI, both of which have available Dart packages.</p>
<h2 id="heading-setting-up-the-project">Setting up the project</h2>
<p>To keep things simple, we'll build our ChatGPT clone as a simple API with two endpoints: one for "embedding" new information, where we upload any documents/text we would like to ask questions on, and a chat endpoint to ask questions and get answers based on the uploaded documents.</p>
<p>We will use Dart and the <code>shelf</code> package for the API. Supabase will be used to store the documents as well as their <a target="_blank" href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings">embeddings</a>, which will be used to perform similarity search thanks to Supabase's <code>pgvector</code> plugin.</p>
<p>Let's create our new Dart project:</p>
<pre><code class="lang-sh">dart create dart_ai_api
<span class="hljs-built_in">cd</span> dart_ai_api
</code></pre>
<p>Our first dependencies to build out the API are <code>shelf</code>, <code>shelf_router</code> and <code>dart_openai</code>:</p>
<pre><code class="lang-sh">dart pub add shelf
dart pub add shelf_router
dart pub add dart_openai
</code></pre>
<p>Next, we're adding Supabase as a dependency, but also initializing a Supabase project within our Dart project.</p>
<pre><code class="lang-sh">dart pub add supabase
supabase init
</code></pre>
<p>For additional information on the Supabase project initialization, check out the documentation <a target="_blank" href="https://supabase.com/docs/reference/cli/supabase-init">here</a>.</p>
<h2 id="heading-supabase-vector">Supabase Vector</h2>
<p>Supabase Vector is actually a set of tools to help you build AI apps, and what we'll actually be using for this project is <code>pgvector</code>, a Postgres extension which allows you to store and query vectors in Postgres. And if you're new to Supabase, the Supabase service includes a Postgres database!</p>
<h3 id="heading-preparing-the-database">Preparing the database</h3>
<p>In the seed file, we will create a table to store documents and embeddings, as well as a function to perform similarity search. First, we need to enable the <code>pgvector</code> plugin.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- supabase/seed.sql</span>
<span class="hljs-keyword">create</span> extension vector <span class="hljs-keyword">with</span> <span class="hljs-keyword">schema</span> extensions;
</code></pre>
<p>Next, the documents table:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- supabase/seed.sql</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> documents (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">serial</span> primary <span class="hljs-keyword">key</span>,
  title <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">body</span> <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  embedding vector(<span class="hljs-number">1536</span>)
);
</code></pre>
<p>And last but not least, the similarity search function:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- supabase/seed.sql</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">function</span> match_documents (
  query_embedding vector(<span class="hljs-number">1536</span>),
  match_threshold <span class="hljs-built_in">float</span>,
  match_count <span class="hljs-built_in">int</span>
)
<span class="hljs-keyword">returns</span> <span class="hljs-keyword">table</span> (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">bigint</span>,
  <span class="hljs-keyword">body</span> <span class="hljs-built_in">text</span>,
  similarity <span class="hljs-built_in">float</span>
)
<span class="hljs-keyword">language</span> <span class="hljs-keyword">sql</span> stable
<span class="hljs-keyword">as</span> $$
  <span class="hljs-keyword">select</span>
    documents.id,
    documents.body,
    <span class="hljs-number">1</span> - (documents.embedding &lt;=&gt; query_embedding) <span class="hljs-keyword">as</span> similarity
  <span class="hljs-keyword">from</span> documents
  <span class="hljs-keyword">where</span> <span class="hljs-number">1</span> - (documents.embedding &lt;=&gt; query_embedding) &gt; match_threshold
  <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> similarity <span class="hljs-keyword">desc</span>
  <span class="hljs-keyword">limit</span> match_count;
$$;
</code></pre>
<p>There is a lot to explain, but the <a target="_blank" href="https://supabase.com/docs/guides/ai/vector-columns">documentation</a> over at Supabase does a good job introducing all the concepts. So I won't be going too in-depth on the details and rather focusing on getting this to work in Dart.</p>
<p>At this point we could run our Supabase project locally, which will create the database including the documents table and function.</p>
<pre><code class="lang-sh">supabase start
</code></pre>
<h2 id="heading-the-dart-api">The Dart API</h2>
<p>Let's build the Dart API next. We'll start with an AI service class, which will handle the interactions with Supabase and OpenAI. This service will then be called by the endpoint handlers.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AIService</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _openAiKey = <span class="hljs-string">"SECRET"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _supabaseUrl = <span class="hljs-string">"http://localhost:54321"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _supabaseKey = <span class="hljs-string">"SECRET"</span>;

  <span class="hljs-keyword">final</span> OpenAI openAi;
  <span class="hljs-keyword">final</span> SupabaseClient supabase;

  <span class="hljs-keyword">factory</span> AIService.init() {
    OpenAI.apiKey = _openAiKey;
    <span class="hljs-keyword">final</span> supabase = SupabaseClient(_supabaseUrl, _supabaseKey);
    <span class="hljs-keyword">return</span> AIService(OpenAI.instance, supabase);
  }

  AIService(<span class="hljs-keyword">this</span>.openAi, <span class="hljs-keyword">this</span>.supabase);
}
</code></pre>
<p>The embed method will receive a document's title and body, and store this data along with the embeddings created by calling the OpenAI API. For the embedding model we use <code>text-embedding-ada-002</code>, which is recommended by OpenAI for most use cases. Note that we should use the same model for all embeddings, otherwise similarity search will not work.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> embed(EmbedRequest request) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> openAi.embedding.create(
    model: <span class="hljs-string">"text-embedding-ada-002"</span>,
    input: request.body,
  );
  <span class="hljs-keyword">final</span> embedding = response.data.first.embeddings;
  <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"documents"</span>).insert({
    <span class="hljs-string">"title"</span>: request.title,
    <span class="hljs-string">"body"</span>: request.body,
    <span class="hljs-string">"embedding"</span>: embedding,
  });
}
</code></pre>
<p>The second and last method of our AI service is the chat functionality. It does several things:</p>
<ul>
<li>Creates an embedding of the question we pass to it, similar to the embed functionality previously.</li>
<li>Uses that embedding to perform similarity search on our existing saved documents, using the function we created during the database setup step.</li>
<li>Calls the OpenAI Chat API with a system prompt including any relevant documents found as the context, as well as the question.</li>
</ul>
<pre><code class="lang-dart">Future&lt;<span class="hljs-built_in">String</span>&gt; chat(ChatRequest request) <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// create an embedding of the message to perform similarity search</span>
  <span class="hljs-keyword">final</span> embeddingResponse = <span class="hljs-keyword">await</span> openAi.embedding.create(
    model: <span class="hljs-string">"text-embedding-ada-002"</span>,
    input: request.message,
  );
  <span class="hljs-keyword">final</span> embedding = embeddingResponse.data.first.embeddings;

  <span class="hljs-comment">// retrieve up to 5 most similar documents to include in chat system prompt</span>
  <span class="hljs-keyword">final</span> results = <span class="hljs-keyword">await</span> supabase.rpc(<span class="hljs-string">'match_documents'</span>, params: {
    <span class="hljs-string">'query_embedding'</span>: embedding,
    <span class="hljs-string">'match_threshold'</span>: <span class="hljs-number">0.8</span>,
    <span class="hljs-string">'match_count'</span>: <span class="hljs-number">5</span>,
  });

  <span class="hljs-comment">// combine document content together</span>
  <span class="hljs-keyword">var</span> context = <span class="hljs-string">''</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> <span class="hljs-built_in">document</span> <span class="hljs-keyword">in</span> results) {
    <span class="hljs-built_in">document</span> <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">final</span> content = <span class="hljs-built_in">document</span>[<span class="hljs-string">'body'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String</span>;
    context += <span class="hljs-string">'<span class="hljs-subst">$content</span>\n---\n'</span>;
  }

  <span class="hljs-keyword">final</span> prompt = <span class="hljs-string">"""
      You are a helpful AI assistant.
      Given the following sections, answer any user questions by
      using only that information.
      If you are unsure and the answer is not explicitly written in
      the sections below, say "Sorry, I can't help you with that."

      Context sections:
      <span class="hljs-subst">$context</span>
    """</span>;

  <span class="hljs-keyword">final</span> chatResponse =
      <span class="hljs-keyword">await</span> openAi.chat.create(model: <span class="hljs-string">'gpt-3.5-turbo'</span>, messages: [
    OpenAIChatCompletionChoiceMessageModel(
        role: OpenAIChatMessageRole.system, content: prompt),
    OpenAIChatCompletionChoiceMessageModel(
        role: OpenAIChatMessageRole.user, content: request.message),
  ]);
  <span class="hljs-keyword">return</span> chatResponse.choices.first.message.content;
}
</code></pre>
<h3 id="heading-why-similarity-search">Why similarity search?</h3>
<p>A request to OpenAI's Chat API has a maximum token limit. We cannot just give it all our documents as the context in the prompt. Because of this, similarity search through vector embeddings is a solution for this. Of course, this depends on the total number of documents, and if there's only a few, this could still be possible. Additionally, the more tokens that are part of the request, the more expensive the API call.</p>
<p>In the <code>chat</code> method above, you can see from the RPC call to Supabase that we are only returning the 5 most relevant documents that are past the threshold. Both the threshold and count parameters can be tweaked as desired. By only passing the few most relevant documents to OpenAI, we can get a better, faster, and cheaper response.</p>
<h2 id="heading-api-routes">API routes</h2>
<p>The last piece is the router which route the API requests to the appropriate AI service method. The <code>shelf</code> and <code>shelf_router</code> packages make this quite easy.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> run() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> app = Router();
  <span class="hljs-keyword">final</span> ai = AIService.init();

  app.post(<span class="hljs-string">'/embed'</span>, (Request request) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> json = <span class="hljs-keyword">await</span> _requestToJson(request);
      ai.embed(EmbedRequest.fromJson(json));
      <span class="hljs-keyword">return</span> Response.ok(<span class="hljs-string">'embedding saved'</span>);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">print</span>(err);
      <span class="hljs-keyword">return</span> Response.internalServerError(body: <span class="hljs-string">'something went wrong'</span>);
    }
  });

  app.post(<span class="hljs-string">'/chat'</span>, (Request request) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> json = <span class="hljs-keyword">await</span> _requestToJson(request);
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> ai.chat(ChatRequest.fromJson(json));
      <span class="hljs-keyword">return</span> Response.ok(response);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">print</span>(err);
      <span class="hljs-keyword">return</span> Response.internalServerError(body: <span class="hljs-string">'something went wrong'</span>);
    }
  });

  <span class="hljs-keyword">final</span> server = <span class="hljs-keyword">await</span> io.serve(app, <span class="hljs-string">'localhost'</span>, <span class="hljs-number">8080</span>);
  <span class="hljs-built_in">print</span>(<span class="hljs-string">'Server running on localhost:<span class="hljs-subst">${server.port}</span>'</span>);
}

Future&lt;<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;&gt; _requestToJson(Request request) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> reqString = <span class="hljs-keyword">await</span> request.readAsString();
  <span class="hljs-keyword">return</span> jsonDecode(reqString);
}
</code></pre>
<p>After this, we are now ready to run our Dart server. Make sure to start Supabase as well if not already running.</p>
<pre><code class="lang-sh">supabase start
dart run
</code></pre>
<h2 id="heading-the-api-in-action">The API in action</h2>
<p>We can now store documents by calling the <code>/embed</code> endpoint:</p>
<pre><code class="lang-sh">curl --header <span class="hljs-string">"Content-Type: application/json"</span> \
  --request POST \
  --data <span class="hljs-string">'{"title":"Flutter","body":"Flutter is an open-source, cross-platform UI software development kit"}'</span> \
  http://localhost:8080/embed
</code></pre>
<p>And ask questions by calling the <code>/chat</code> endpoint:</p>
<pre><code class="lang-sh">curl --header <span class="hljs-string">"Content-Type: application/json"</span> \
  --request POST \
  --data <span class="hljs-string">'{"message":"What is Flutter?"}'</span> \
  http://localhost:8080/chat
</code></pre>
<p>The answer to the above question should be taken from the previous data we stored! We can now use this API to store any kind of information we want, and ask questions about it.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this post, we built our own ChatGPT API which we can use to store textual data and ask questions on it, all in Dart, and with the help of Supabase and the OpenAI API. You can find the full source code <a target="_blank" href="https://github.com/dartling/dart_api_api">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Full-stack Dart with Flutter, Supabase and Dart Edge]]></title><description><![CDATA[Introduction
Supabase has recently had its 7th Launch Week, with lots of new features and functionality shipped by the Supabase team as well as the community.
One of the community highlights was Dart Edge for Supabase Edge Functions. Built by Inverta...]]></description><link>https://dartling.dev/full-stack-dart-with-flutter-supabase-and-dart-edge</link><guid isPermaLink="true">https://dartling.dev/full-stack-dart-with-flutter-supabase-and-dart-edge</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[supabase]]></category><category><![CDATA[edge]]></category><category><![CDATA[Edge-Functions]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 25 Apr 2023 22:22:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682454346048/217067b6-8c67-4e3b-b800-96377b750c4f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p><a target="_blank" href="https://supabase.com">Supabase</a> has recently had its <a target="_blank" href="https://supabase.com/launch-week">7th Launch Week</a>, with lots of new features and functionality shipped by the Supabase team as well as <a target="_blank" href="https://supabase.com/blog/launch-week-7-community-highlights">the community</a>.</p>
<p>One of the community highlights was <a target="_blank" href="https://docs.dartedge.dev/">Dart Edge</a> for Supabase Edge Functions. Built by <a target="_blank" href="https://invertase.io">Invertase</a>, Dart Edge allows you to write your edge functions in Dart. It supports Vercel, Cloudflare Workers, and now Supabase Edge Functions, which until recently only supported writing functions with TypeScript. However, it is still experimental!</p>
<p>Being able to write our edge functions with Dart is amazing news for Flutter developers. It allows us to write Dart for both the front-end and any back-end code we need, which also means we can share Dart code, such as the request/response models. And while Supabase's <a target="_blank" href="https://supabase.com/docs/guides/database/functions">database functions</a> can be quite powerful, an edge function can do additional things such as call third-party APIs and services, and even bypass RLS policies if necessary.</p>
<p>In this post, we will explore Dart Edge with Supabase Edge Functions. We will build a simple app with Dart on the full stack, using a Flutter app front-end and a Dart Edge function as our back-end. We'll keep things simple by starting with the classic Flutter counter app but using an edge function to get the current count and increment it.</p>
<h2 id="heading-set-up-an-edge-function">Set up an edge function</h2>
<p>First, we need both the Supabase CLI and edge CLI. For the edge CLI, simply run:</p>
<pre><code class="lang-bash">dart pub global activate edge
</code></pre>
<p>The Supabase CLI can be installed through NPM or either Homebrew or Scoop depending on your operating system, so check out the documentation <a target="_blank" href="https://supabase.com/docs/guides/cli#installation">here</a>.</p>
<p>Now, let's set up a Dart Edge project:</p>
<pre><code class="lang-bash">edge new supabase_functions edge_functions
<span class="hljs-built_in">cd</span> edge_functions
supabase init
</code></pre>
<p><code>edge_functions</code> is the name of our project and the name of the newly created directory. We run <code>supabase init</code> to generate the Supabase config.</p>
<p>The newly generated <code>edge_functions</code> directory is a typical Dart project with a <code>pubspec.yaml</code> file with the required dependencies. It has a <code>main.dart</code> file with a simple response to get us started:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// edge_functions/lib/main.dart</span>
<span class="hljs-keyword">void</span> main() {
  SupabaseFunctions(fetch: (request) {
    <span class="hljs-keyword">return</span> Response(<span class="hljs-string">"Hello from Supabase Edge Functions!"</span>);
  });
}
</code></pre>
<p>To run the edge function locally, we need to build the function and run Supabase:</p>
<pre><code class="lang-bash">edge build supabase_functions
supabase start
supabase <span class="hljs-built_in">functions</span> serve dart_edge --no-verify-jwt
</code></pre>
<p>We can also use the <code>--dev</code> flag when running <code>edge build</code> to recompile when we make any changes to the code.</p>
<p>By default, the edge function is expected to have a valid authorization header. We pass the <code>--no-verify-jwt</code> flag so we can run the function without one.</p>
<p>We can now access the local edge function at <a target="_blank" href="http://localhost:54321/functions/v1/dart_edge">http://localhost:54321/functions/v1/dart_edge</a></p>
<p>To deploy the function, we can build it and then run the following command:</p>
<pre><code class="lang-bash">supabase <span class="hljs-built_in">functions</span> deploy dart_edge
</code></pre>
<h2 id="heading-connect-to-supabase-from-the-edge">Connect to Supabase from the edge</h2>
<p>The current edge function only returns some text in the response. In a real app, we would likely want to interact with our database, make some third-party service calls, or both.</p>
<pre><code class="lang-bash">dart pub add supabase
dart pub add edge_http_client
</code></pre>
<p>The <code>supabase</code> package has all we need to interact with the Supabase database, but we also need to use a special HTTP client.</p>
<p>We can now instantiate a Supabase client:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> supabase = SupabaseClient(
  Deno.env.<span class="hljs-keyword">get</span>(<span class="hljs-string">'SUPABASE_URL'</span>)!,
  Deno.env.<span class="hljs-keyword">get</span>(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>)!,
  httpClient: EdgeHttpClient(),
);
</code></pre>
<p>A couple of notes on the above snippet:</p>
<ul>
<li><p>The Supabase credentials are already available by default in the environment variables.</p>
</li>
<li><p>The service role key bypasses Row Level Security, meaning we can access and edit all data of all users. Since this is for an edge function, it's fine, but we could also use the anon key (<code>SUPABASE_ANON_KEY</code> environment variable) if that's a concern.</p>
</li>
</ul>
<p>We now have access to the Supabase database from our edge function. Let's modify the function's logic to do two things:</p>
<ol>
<li><p>Return the counter's current value if we make a <code>GET</code> request.</p>
</li>
<li><p>Increment the counter by 1 if we make a <code>POST</code> request.</p>
</li>
</ol>
<p>We'll first need to create the database table that will hold our increments. These SQL statements are added to the <code>seed.sql</code> file.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- edge_functions/supabase/seed.sql</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> increments (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">bigint</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">identity</span> primary <span class="hljs-keyword">key</span>,
  amount <span class="hljs-built_in">int</span> <span class="hljs-keyword">default</span> <span class="hljs-number">0</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">function</span> get_count()
<span class="hljs-keyword">returns</span> <span class="hljs-built_in">numeric</span> <span class="hljs-keyword">as</span> $$
  <span class="hljs-keyword">select</span> <span class="hljs-keyword">sum</span>(amount) <span class="hljs-keyword">from</span> increments;
$$ language sql;
</code></pre>
<p>We also create a SQL function that returns the current count by summing up all the increments.</p>
<p>Now let's update our edge function's <code>main</code> method:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// edge_functions/lib/model/counter_response.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterResponse</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> count;

  CounterResponse(<span class="hljs-keyword">this</span>.count);

  <span class="hljs-keyword">factory</span> CounterResponse.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) =&gt;
      CounterResponse(json[<span class="hljs-string">'count'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">int</span>);

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; <span class="hljs-keyword">get</span> toJson =&gt; {<span class="hljs-string">'count'</span>: count};
}

<span class="hljs-comment">// edge_functions/lib/main.dart</span>
<span class="hljs-keyword">void</span> main() {
  SupabaseFunctions(fetch: (request) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> client = SupabaseClient(
      Deno.env.<span class="hljs-keyword">get</span>(<span class="hljs-string">'SUPABASE_URL'</span>)!,
      Deno.env.<span class="hljs-keyword">get</span>(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>)!,
      httpClient: EdgeHttpClient(),
    );

    <span class="hljs-keyword">if</span> (request.method == <span class="hljs-string">'POST'</span>) {
      <span class="hljs-keyword">await</span> client.from(<span class="hljs-string">'increments'</span>).insert({<span class="hljs-string">'amount'</span>: <span class="hljs-number">1</span>});
    }
    <span class="hljs-keyword">final</span> count = <span class="hljs-keyword">await</span> client.rpc(<span class="hljs-string">'count'</span>).single() <span class="hljs-keyword">as</span> <span class="hljs-built_in">int</span>;
    <span class="hljs-keyword">final</span> response = CounterResponse(count);
    <span class="hljs-keyword">return</span> Response.json(response.toJson);
  });
}
</code></pre>
<p>If the request is a <code>POST</code>, we increment the count by 1. For all requests, we fetch the current count and return it in the response. To fetch the count, we're calling a SQL function within an edge function!</p>
<h2 id="heading-call-the-function-from-flutter">Call the function from Flutter</h2>
<p>With the edge function ready to go, we will now create the Flutter counter app, and update it so that the count display and increment button work by calling the edge function.</p>
<pre><code class="lang-bash">flutter create app
</code></pre>
<p>We'll be calling the function with the <code>http</code> package, but we're also going to need to add the <code>edge_functions</code> package as a local dependency. This is so we can reuse the <code>CounterResponse</code> model.</p>
<p>Here are the current dependencies (<code>pubspec.yaml</code>) for the Flutter app:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">counter_edge_functions:</span>
    <span class="hljs-attr">path:</span>  <span class="hljs-string">'../edge_functions'</span>
  <span class="hljs-attr">http:</span> <span class="hljs-string">^0.13.5</span>
</code></pre>
<p>At the moment of writing, there seems to be a dependency issue due to the <code>path</code> package, so I had to remove <code>flutter_test</code> as a dev dependency to get this to work. A nicer fix for this would be to move the model to a separate local package instead and have it as a dependency for both the edge function package and the Flutter app.</p>
<p>Let's also encapsulate the interactions with the edge function to a repository:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:convert'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:counter_edge_functions/model/counter_response.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:http/http.dart'</span> <span class="hljs-keyword">as</span> http;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterRepository</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> _functionUrl =
      <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'http://localhost:54321/functions/v1/dart_edge'</span>);

  Future&lt;CounterResponse&gt; <span class="hljs-keyword">get</span> count <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(_functionUrl);
    <span class="hljs-keyword">final</span> body =
        jsonDecode(utf8.decode(response.bodyBytes)) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">return</span> CounterResponse.fromJson(body);
  }

  Future&lt;CounterResponse&gt; increment() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(_functionUrl);
    <span class="hljs-keyword">final</span> body =
        jsonDecode(utf8.decode(response.bodyBytes)) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">return</span> CounterResponse.fromJson(body);
  }
}
</code></pre>
<p>The final piece is to call the repository methods from our widgets. There are not that many changes compared to the generated counter app code from the template, but here is the final result:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MyApp({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> repository = CounterRepository();
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Edge Counter'</span>,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      <span class="hljs-comment">// Fetch the initial count from the Edge Function.</span>
      home: FutureBuilder&lt;CounterResponse&gt;(
        future: repository.count,
        builder: (context, snapshot) {
          <span class="hljs-keyword">if</span> (snapshot.connectionState != ConnectionState.done) {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> SizedBox();
          }
          <span class="hljs-keyword">return</span> MyHomePage(
            title: <span class="hljs-string">'Edge Counter'</span>,
            initialCount: snapshot.data?.count ?? <span class="hljs-number">0</span>,
            onIncrement: repository.increment,
          );
        },
      ),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyHomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> MyHomePage({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.initialCount,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onIncrement,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> initialCount;
  <span class="hljs-keyword">final</span> Future&lt;CounterResponse&gt; <span class="hljs-built_in">Function</span>() onIncrement;

  <span class="hljs-meta">@override</span>
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyHomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyHomePage</span>&gt; </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-built_in">int</span> _counter;

  <span class="hljs-comment">// Increment through the Edge Function, and update with the latest count.</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; _incrementCounter() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> widget.onIncrement();
    setState(() {
      _counter = response.count;
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _counter = widget.initialCount;
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            <span class="hljs-keyword">const</span> Text(
              <span class="hljs-string">'Users have pushed the button this many times:'</span>,
            ),
            Text(
              <span class="hljs-string">'<span class="hljs-subst">$_counter</span>'</span>,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: <span class="hljs-string">'Increment'</span>,
        child: <span class="hljs-keyword">const</span> Icon(Icons.add),
      ),
    );
  }
}
</code></pre>
<p>We wrap the home page in a <code>FutureBuilder</code> to start with the current counter value, and call the increment method on button click. The count persists across restarts, and if we were using a deployed function rather than a local one, we could even share the app with friends and have everyone increment it at the same time!</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this post, we talked about the experimental Dart Edge, allowing us to write Dart on the full stack - by using Flutter and Supabase. We created a very simple edge function that interacts with our database, and an even simpler Flutter counter app that calls the edge function to increment the counter.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/edge_counter">here</a>.</p>
<p>If you found this helpful and would like to be notified of future tutorials, please follow the blog or subscribe to the newsletter with your email below!</p>
]]></content:encoded></item><item><title><![CDATA[Building a ChatGPT client app with Flutter]]></title><description><![CDATA[Introduction
In this blog post, we will build a simple conversational interface to chat with OpenAI's ChatGPT through its API.
There has been a lot of hype with OpenAI and ChatGPT lately, especially with GPT-4 being released recently. A ton of use ca...]]></description><link>https://dartling.dev/building-a-chatgpt-client-app-with-flutter</link><guid isPermaLink="true">https://dartling.dev/building-a-chatgpt-client-app-with-flutter</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[openai]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Fri, 17 Mar 2023 13:01:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679053988679/4b0fa6f2-a6c0-4186-a77b-f907d59c1ef6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, we will build a simple conversational interface to chat with OpenAI's ChatGPT through its API.</p>
<p>There has been a lot of hype with OpenAI and ChatGPT lately, especially with GPT-4 being released recently. A ton of use cases for such tools have been popping up, but the most popular way people have been using ChatGPT so far is through <a target="_blank" href="https://chat.openai.com/chat">chat.openai.com</a>. I've been using ChatGPT to brainstorm ideas, write some Flutter code snippets, and even write the outline for this blog post! Of course, its suggested outline was quite optimistic so I had to leave some sections out, but it still provided enough pointers for me to be able to get started right away.</p>
<p>The official chat experience at OpenAI's chat interface is not great, however. It's very limited, and the chat history is often not working properly. There are already people building client apps for ChatGPT with a better UI and user experience, such as <a target="_blank" href="https://www.typingmind.com/">TypingMind</a>, built with web technologies.</p>
<p>As a Flutter developer, I can't help but think that Flutter is a great fit for a ChatGPT client app! With cross-platform capabilities and a rich set of UI components, Flutter is a perfect choice for such a project. We can write the code once, and we could publish our app on the web, iOS, Android, and also the desktop platforms: Windows, macOS and Linux.</p>
<h2 id="heading-the-chatgpt-api">The ChatGPT API</h2>
<p>To use any of OpenAI's APIs, you'll need to sign up and get an API key. You can do this <a target="_blank" href="https://platform.openai.com/signup">here</a>. Please note that API usage can cost money and you will need to provide payment details. The <code>gpt-3.5-turbo</code> model specifically, which we will be using, is quite cheap and should not cost more than a few cents unless you use it a lot.</p>
<p>Specifically, we will be using the Chat API (chat completions), which supports two of OpenAI's models: <code>gpt-3.5-turbo</code> and <code>gpt-4</code>. We can find the full reference for the Chat API <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat">here</a>, which involves performing a <code>POST</code> request at <code>https://api.openai.com/v1/chat/completions</code>.</p>
<p>At this point, we could use the <code>http</code> library to perform the request with the required data to the Chat API, and parse the response. However, thanks to the Dart and Flutter communities, there is already a package available on pub.dev: <a target="_blank" href="https://pub.dev/packages/dart_openai">dart_openai</a>. It will make the API request for us and return a parsed response, so we can simply grab the response text and display it in the app.</p>
<p>Here is what a method that accepts a user message and returns ChatGPT's response looks like:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-built_in">String</span>&gt; completeChat(<span class="hljs-built_in">String</span> message) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> chatCompletion = <span class="hljs-keyword">await</span> OpenAI.instance.chat.create(
    model: <span class="hljs-string">'gpt-3.5-turbo'</span>,
    messages: [
      OpenAIChatCompletionChoiceMessageModel(
        content: message,
        role: <span class="hljs-string">'user'</span>,
      ),
    ],
  );
  <span class="hljs-keyword">return</span> chatCompletion.choices.first.message.content;
}
</code></pre>
<p>Since this will be a conversation, we need to pass previous messages to the request, so that ChatGPT has the whole context of the conversation so far, and not just the user's last message.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatMessage</span> </span>{
  ChatMessage(<span class="hljs-keyword">this</span>.content, <span class="hljs-keyword">this</span>.isUserMessage);

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> content;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isUserMessage;
}

Future&lt;<span class="hljs-built_in">String</span>&gt; completeChat(<span class="hljs-built_in">List</span>&lt;ChatMessage&gt; messages) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> chatCompletion = <span class="hljs-keyword">await</span> OpenAI.instance.chat.create(
    model: <span class="hljs-string">'gpt-3.5-turbo'</span>,
    messages: [
      ...previousMessages.map(
        (e) =&gt; OpenAIChatCompletionChoiceMessageModel(
          role: e.isUserMessage ? <span class="hljs-string">'user'</span> : <span class="hljs-string">'assistant'</span>,
          content: e.content,
        ),
      ),
    ],
  );
  <span class="hljs-keyword">return</span> chatCompletion.choices.first.message.content;
}
</code></pre>
<p>The above method accepts the user's last message and all previous messages in the conversation. Note that ChatGPT's responses are marked with the role <code>assistant</code> in the API request.</p>
<p>Let's put the final version of our <code>completeChat</code> method to a <code>ChatApi</code> class, to be used later.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// models/chat_message.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatMessage</span> </span>{
  ChatMessage(<span class="hljs-keyword">this</span>.content, <span class="hljs-keyword">this</span>.isUserMessage);

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> content;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isUserMessage;
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// api/chat_api.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/models/chat_message.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/secrets.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_openai/openai.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatApi</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _model = <span class="hljs-string">'gpt-3.5-turbo'</span>;

  ChatApi() {
    OpenAI.apiKey = openAiApiKey;
    OpenAI.organization = openAiOrg;
  }

  Future&lt;<span class="hljs-built_in">String</span>&gt; completeChat(<span class="hljs-built_in">List</span>&lt;ChatMessage&gt; messages) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> chatCompletion = <span class="hljs-keyword">await</span> OpenAI.instance.chat.create(
      model: _model,
      messages: messages
          .map((e) =&gt; OpenAIChatCompletionChoiceMessageModel(
                role: e.isUserMessage ? <span class="hljs-string">'user'</span> : <span class="hljs-string">'assistant'</span>,
                content: e.content,
              ))
          .toList(),
    );
    <span class="hljs-keyword">return</span> chatCompletion.choices.first.message.content;
  }
}
</code></pre>
<p>Note that in the constructor, we are setting the API key and organization ID. Without the API key, any request will fail. Organization ID is optional and can be provided in case you have multiple organizations set up with the OpenAI platform.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// secrets.dart</span>
<span class="hljs-keyword">const</span> openAiApiKey = <span class="hljs-string">'YOUR_API_KEY'</span>;
<span class="hljs-keyword">const</span> openAiOrg = <span class="hljs-string">'YOUR_ORGANIZATION_ID'</span>;
</code></pre>
<p>The secrets file is included in <code>.gitignore</code> to avoid committing it to version control. In the project repository on GitHub, a <code>secrets_example.dart</code> file is provided with placeholder values.</p>
<h3 id="heading-a-note-on-api-keys">A note on API keys</h3>
<p>In this post, we are building a <em>client</em> app. An app like this one with the API key hard-coded should not be published. Since API usage can cost, you don't want to expose your API key.</p>
<p>If you want to publish such an app, you have two options:</p>
<ol>
<li><p>Allow users to provide their own API key to start chatting. Users can provide their key through the app and you can securely store it in local storage, to be used in every API request.</p>
</li>
<li><p>Rather than calling the Chat API directly, call a server, or edge function which will then call the Chat API with your own token. This way, you won't expose your API key, can control the traffic, as well as have additional authorization and rate-limiting requirements. If you go with this approach, you might want to consider monetizing, as users who use the app a lot will cost you money!</p>
</li>
</ol>
<h2 id="heading-the-chat-interface">The chat interface</h2>
<p>With the Chat API ready to be used, it's time to build the UI. If you're starting from scratch, you can use <code>flutter create</code> to initialize a Flutter project:</p>
<pre><code class="lang-bash">flutter create my_chatgpt_client
</code></pre>
<p>The UI will be pretty standard and will contain two main widgets: the message composer, and the message bubble. The main screen will be a list of all messages in the chat (as message bubbles), with the message composer at the bottom where we can type in the messages.</p>
<p>Let's start with the message composer widget:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// widgets/message_composer.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageComposer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  MessageComposer({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onSubmitted,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.awaitingResponse,
    <span class="hljs-keyword">super</span>.key,
  });

  <span class="hljs-keyword">final</span> TextEditingController _messageController = TextEditingController();

  <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-built_in">Function</span>(<span class="hljs-built_in">String</span>) onSubmitted;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> awaitingResponse;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Container(
      padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">12</span>),
      color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(<span class="hljs-number">0.05</span>),
      child: SafeArea(
        child: Row(
          children: [
            Expanded(
              child: !awaitingResponse
                  ? TextField(
                      controller: _messageController,
                      onSubmitted: onSubmitted,
                      decoration: <span class="hljs-keyword">const</span> InputDecoration(
                        hintText: <span class="hljs-string">'Write your message here...'</span>,
                        border: InputBorder.none,
                      ),
                    )
                  : Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <span class="hljs-keyword">const</span> [
                        SizedBox(
                          height: <span class="hljs-number">24</span>,
                          width: <span class="hljs-number">24</span>,
                          child: CircularProgressIndicator(),
                        ),
                        Padding(
                          padding: EdgeInsets.all(<span class="hljs-number">16</span>),
                          child: Text(<span class="hljs-string">'Fetching response...'</span>),
                        ),
                      ],
                    ),
            ),
            IconButton(
              onPressed: !awaitingResponse
                  ? () =&gt; onSubmitted(_messageController.text)
                  : <span class="hljs-keyword">null</span>,
              icon: <span class="hljs-keyword">const</span> Icon(Icons.send),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>The message composer will call the <code>onSubmitted</code> method we pass to it when the text field is submitted (e.g. by pressing Enter), or when we tap on the send button on the right. With the <code>awaitingResponse</code> flag, we can hide the text field and disable the send button. We will set this flag to <code>true</code> while a message submission is in progress and we await the API's response.</p>
<p>The message bubble widget is a simple container, with a different background color and sender name depending on whether it's a user message or an AI-generated one:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// widgets/message_bubble.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageBubble</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MessageBubble({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.content,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.isUserMessage,
    <span class="hljs-keyword">super</span>.key,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> content;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isUserMessage;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> themeData = Theme.of(context);
    <span class="hljs-keyword">return</span> Container(
      margin: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8</span>),
      decoration: BoxDecoration(
        color: isUserMessage
            ? themeData.colorScheme.primary.withOpacity(<span class="hljs-number">0.4</span>)
            : themeData.colorScheme.secondary.withOpacity(<span class="hljs-number">0.4</span>),
        borderRadius: <span class="hljs-keyword">const</span> BorderRadius.all(Radius.circular(<span class="hljs-number">12</span>)),
      ),
      child: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">12</span>),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text(
                  isUserMessage ? <span class="hljs-string">'You'</span> : <span class="hljs-string">'AI'</span>,
                  style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                ),
              ],
            ),
            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">8</span>),
            Text(content),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>We now have all the necessary smaller widgets, now let's put them all together on the main page.</p>
<p>Here is what the code for the main chat page looks like:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// chat_page.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/api/chat_api.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/models/chat_message.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/widgets/message_bubble.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/widgets/message_composer.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> ChatPage({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.chatApi,
    <span class="hljs-keyword">super</span>.key,
  });

  <span class="hljs-keyword">final</span> ChatApi chatApi;

  <span class="hljs-meta">@override</span>
  State&lt;ChatPage&gt; createState() =&gt; _ChatPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ChatPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ChatPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _messages = &lt;ChatMessage&gt;[
    ChatMessage(<span class="hljs-string">'Hello, how can I help?'</span>, <span class="hljs-keyword">false</span>),
  ];
  <span class="hljs-keyword">var</span> _awaitingResponse = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Chat'</span>)),
      body: Column(
        children: [
          Expanded(
            child: ListView(
              children: [
                ..._messages.map(
                  (msg) =&gt; MessageBubble(
                    content: msg.content,
                    isUserMessage: msg.isUserMessage,
                  ),
                ),
              ],
            ),
          ),
          MessageComposer(
            onSubmitted: _onSubmitted,
            awaitingResponse: _awaitingResponse,
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>This is a stateful widget that starts with the message "How can I help?", just so we don't start with an empty chat.</p>
<p>The final piece is the <code>_onSubmitted</code> method, called through the message composer when a message is submitted.</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; _onSubmitted(<span class="hljs-built_in">String</span> message) <span class="hljs-keyword">async</span> {
  setState(() {
    _messages.add(ChatMessage(message, <span class="hljs-keyword">true</span>));
    _awaitingResponse = <span class="hljs-keyword">true</span>;
  });
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> widget.chatApi.completeChat(_messages);
  setState(() {
    _messages.add(ChatMessage(response, <span class="hljs-keyword">false</span>));
    _awaitingResponse = <span class="hljs-keyword">false</span>;
  });
}
</code></pre>
<p>When a message is submitted, we add the message to the chat messages and set <code>_awaitingResponse</code> to <code>true</code>, wrapped in a <code>setState</code> call. This will show the user message in the conversation, and disable the message composer.</p>
<p>Next, we pass all messages to the Chat API and await the response. Once we have the response, we add it as a chat message in <code>_messages</code> and set <code>_awaitingResponse</code> back to <code>false</code>, wrapped in a second <code>setState</code> call.</p>
<p>And that's it for the conversation flow! Let's see it in action:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679051699450/50546741-30b4-40d5-b39a-465c06b39905.png" alt class="image--center mx-auto" /></p>
<p>This is the code for the <code>App</code> and the <code>main</code> method:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/api/chat_api.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:chatgpt_client/chat_page.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  runApp(ChatApp(chatApi: ChatApi()));
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ChatApp({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.chatApi, <span class="hljs-keyword">super</span>.key});

  <span class="hljs-keyword">final</span> ChatApi chatApi;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'ChatGPT Client'</span>,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          secondary: Colors.lime,
        ),
      ),
      home: ChatPage(chatApi: chatApi),
    );
  }
}
</code></pre>
<h2 id="heading-parsing-markdown">Parsing markdown</h2>
<p>In our previous conversation with ChatGPT, we ask the follow-up question "show me the code".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679051910822/54528813-4534-48a4-a9ef-d577628dcd59.png" alt class="image--center mx-auto" /></p>
<p>We get a decent amount of Flutter code in the response, but it's all in markdown! Let's use the <code>markdown_widget</code> package to solve this.</p>
<pre><code class="lang-dart">flutter pub add markdown_widget
</code></pre>
<p>In the <code>MessageBubble</code> widget, replace the <code>Text</code> widget containing the message content with a <code>MarkdownWidget</code>:</p>
<pre><code class="lang-dart">MarkdownWidget(
  data: content,
  shrinkWrap: <span class="hljs-keyword">true</span>,
)
</code></pre>
<p>One hot reload later, and we can see that the code is now properly parsed. That was easy!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679052226201/f2899176-36b7-4277-aa15-6a3a39fa089e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-error-handling">Error handling</h2>
<p>What if we get an error response from OpenAI? While testing, I did run into a few 429 (Too Many Requests) exceptions. Such an error could happen if you are calling the API too often, but also if the OpenAI API is getting too many requests in general.</p>
<p>The least we can do is handle the error and display a useful message. Here's a revised <code>_onSubmitted</code> method:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; _onSubmitted(<span class="hljs-built_in">String</span> message) <span class="hljs-keyword">async</span> {
  setState(() {
    _messages.add(ChatMessage(message, <span class="hljs-keyword">true</span>));
    _awaitingResponse = <span class="hljs-keyword">true</span>;
  });
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> widget.chatApi.completeChat(_messages);
    setState(() {
      _messages.add(ChatMessage(response, <span class="hljs-keyword">false</span>));
      _awaitingResponse = <span class="hljs-keyword">false</span>;
    });
  } <span class="hljs-keyword">catch</span> (err) {
    ScaffoldMessenger.of(context).showSnackBar(
      <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'An error occurred. Please try again.'</span>)),
    );
    setState(() {
      _awaitingResponse = <span class="hljs-keyword">false</span>;
    });
  }
}
</code></pre>
<p>Of course, this could be improved even further. We can provide an option to retry the response without needing to send a new message, but also automatically retry the request in <code>ChatApi</code> without showing an error.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>We now have a fully working chat app to chat with ChatGPT at any time, on any platform!</p>
<p>In this post, we showed how to build a basic chat app to have conversations with ChatGPT through OpenAI's chat API. We also added some additional features such as markdown parsing and error handling.</p>
<p>The functionality is quite basic, but there's a lot more we can do with such an app. We could have useful features such as being able to copy and/or share the responses. Additionally, we can use a local or cloud database to store the conversations so we can access them at any time.</p>
<p>You can find the source code <a target="_blank" href="https://github.com/dartling/chatgpt_client">here</a>.</p>
<p>Are you interested in building a ChatGPT client, or building a Flutter app utilizing ChatGPT? Or would you be interested in any follow-up articles where we improve this app by adding more features such as conversation storage and organization? Let me know in the comments!</p>
<p>If you found this helpful and would like to be notified of future posts and tutorials, please subscribe to the newsletter with your email below!</p>
]]></content:encoded></item><item><title><![CDATA[Building a multiplayer quiz game with Flutter and Supabase]]></title><description><![CDATA[Introduction
During the Supabase Launch Week 6 Hackathon, I decided to take on the challenge of building a multiplayer quiz game using Flutter and Supabase.
It had been a while since I wrote something about Supabase, and there's been a lot of changes...]]></description><link>https://dartling.dev/building-a-multiplayer-quiz-game-with-flutter-and-supabase</link><guid isPermaLink="true">https://dartling.dev/building-a-multiplayer-quiz-game-with-flutter-and-supabase</guid><category><![CDATA[Flutter]]></category><category><![CDATA[supabase]]></category><category><![CDATA[hackathon]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Mon, 16 Jan 2023 18:05:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1673644254533/e6e1fc77-7dc2-4dde-a303-945f464699d3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>During the <a target="_blank" href="https://supabase.com/blog/launch-week-6-hackathon">Supabase Launch Week 6 Hackathon</a>, I decided to take on the challenge of building a multiplayer quiz game using Flutter and Supabase.</p>
<p>It had <a target="_blank" href="https://dartling.dev/series/full-stack-flutter">been a while</a> since I wrote something about Supabase, and there's been a lot of changes and new functionality I had been looking forward to trying out, especially <a target="_blank" href="https://supabase.com/realtime">Realtime</a>. So a Supabase hackathon was the perfect chance to get back into it!</p>
<p>Working on it a couple of hours at a time over about a week, I managed to build a simple quiz game that you can play with friends at the same time. In this post, I will share the journey of building the quiz game and how I used Supabase to achieve the multiplayer functionality.</p>
<p>If you want to play, you can try out the live demo <a target="_blank" href="https://yallurium.github.io/supaquiz">here</a>; there's also a practice mode in which you can play alone.</p>
<h2 id="heading-building-supaquiz">Building Supaquiz</h2>
<p>The idea was simple; a quiz game that you can play with friends at the same time. With Supabase's Realtime functionality, it seemed achievable to build a functional app with Flutter during the hackathon.</p>
<p>It would take too long to share how I built Supaquiz step-by-step, but I will go over some specific parts that I found interesting and/or challenging. The project is open-source, so you can have a look at the source code <a target="_blank" href="https://github.com/yallurium/supaquiz">here</a>.</p>
<h3 id="heading-user-interface">User interface</h3>
<p>As easy as it is to build user interfaces in Flutter, this is usually my least favorite part as it can take a while to get a screen/widget to look exactly as I want it to. But in this case, the screens were quite simple, so all I had to decide was the main color (I went with what I call "Supabase Green") and a font. A quick search led me to the <a target="_blank" href="https://fonts.google.com/specimen/Press+Start+2P">Press Start 2P</a> font, which fit well.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">const</span> supabaseGreen = Color.fromRGBO(<span class="hljs-number">101</span>, <span class="hljs-number">217</span>, <span class="hljs-number">165</span>, <span class="hljs-number">1.0</span>);

ThemeData <span class="hljs-keyword">get</span> theme {
  <span class="hljs-keyword">final</span> theme = ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: supabaseGreen,
      brightness: Brightness.dark,
    ),
    useMaterial3: <span class="hljs-keyword">true</span>,
  );
  <span class="hljs-keyword">return</span> theme.copyWith(
    textTheme: GoogleFonts.pressStart2pTextTheme(theme.textTheme),
    primaryTextTheme: GoogleFonts.pressStart2pTextTheme(theme.primaryTextTheme),
  );
}a
</code></pre>
<p>With the style decided on, the most complex widget I had to build was the button!</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> label;
  <span class="hljs-keyword">final</span> VoidCallback onPressed;

  <span class="hljs-keyword">const</span> AppButton({
    Key? key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.label,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onPressed,
  }) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Padding(
      padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
      child: Material(
        elevation: <span class="hljs-number">8.0</span>,
        child: OutlinedButton(
          onPressed: onPressed,
          style: OutlinedButton.styleFrom(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(<span class="hljs-number">4.0</span>)),
            ),
            side: BorderSide(
              color: supabaseGreen,
              width: <span class="hljs-number">2</span>,
            ),
          ),
          child: Padding(
            padding:
                <span class="hljs-keyword">const</span> EdgeInsets.symmetric(horizontal: <span class="hljs-number">8.0</span>, vertical: <span class="hljs-number">16.0</span>),
            child: Text(
              label.toUpperCase(),
              style: Theme.of(context).primaryTextTheme.bodyLarge,
            ),
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>And with the buttons done, the rest was easy. Here is what the title screen looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673607140703/05056823-d8ec-4e8c-ae14-635c9d95b62c.png" alt="Screenshot of the Supaquiz title screen" class="image--center mx-auto" /></p>
<p>For multiplayer mode, you can either host a game or join a game. Each game needs a host, though if you wanted you could host a game and start it before anyone else joins... There is also a practice mode if you want to play alone.</p>
<h3 id="heading-the-trivia-api">The Trivia API</h3>
<p>It's a quiz game, so we need questions! <a target="_blank" href="https://the-trivia-api.com">The Trivia API</a> was perfect for this. It's a completely free, simple API, and fetching questions in the app using it was simple.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TriviaRepository</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _authority = <span class="hljs-string">'the-trivia-api.com'</span>;

  Future&lt;<span class="hljs-built_in">List</span>&lt;TriviaQuestion&gt;&gt; getQuestions(<span class="hljs-built_in">int</span> numOfQuestions) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.https(
        _authority, <span class="hljs-string">'api/questions'</span>, {<span class="hljs-string">'limit'</span>: numOfQuestions.toString()});
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(url);
    <span class="hljs-keyword">final</span> questions = jsonDecode(utf8.decode(response.bodyBytes))
        <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;&gt;;
    <span class="hljs-keyword">return</span> questions.map(TriviaQuestion.fromJson).toList();
  }
}
</code></pre>
<p>As you can see above, it's only a simple <code>GET</code> call to the API, with a limit query parameter to return a specific amount of questions. It also supports more customization, such as specific categories for the questions as well as difficulty.</p>
<p>Supaquiz fetches a set of questions before every game and displays the question and possible answers on the screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673607342862/8d8764cd-09c4-46ba-b52b-40c2c47f97e0.png" alt="Screenshot showing the wrong answer in red and correct answer in green" class="image--center mx-auto" /></p>
<p>I promise I got the answer wrong for the sake of the screenshot! If you get the answer wrong it's marked in red, while the correct answer is in a bright "Supabase Green".</p>
<h3 id="heading-setting-up-supabase">Setting up Supabase</h3>
<p>Setting up Supabase was a breeze using the <a target="_blank" href="https://supabase.com/docs/guides/resources/supabase-cli">Supabase CLI</a>, a nice addition since the last time I used Supabase.</p>
<p>The CLI sets up your Supabase config with a simple command, <code>supabase init</code>, and starts up a local instance of Supabase for local development with <code>supabase start</code>.</p>
<p>I added some table creation SQL statements in the <code>seed.sql</code> file generated by the CLI, and was ready to go.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- seed.sql</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">type</span> game_status <span class="hljs-keyword">as</span> enum (<span class="hljs-string">'pending'</span>, <span class="hljs-string">'started'</span>, <span class="hljs-string">'complete'</span>);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> games (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">bigint</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">identity</span> primary <span class="hljs-keyword">key</span>,
  channel <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">default</span> uuid_generate_v4() <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">status</span> game_status <span class="hljs-keyword">default</span> <span class="hljs-string">'pending'</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  seconds_per_question <span class="hljs-built_in">int</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  host_id <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">references</span> auth.users (<span class="hljs-keyword">id</span>) <span class="hljs-keyword">default</span> auth.uid() <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
);
</code></pre>
<h3 id="heading-anonymous-login">Anonymous login</h3>
<p>I was initially thinking of making Supaquiz work without needing users, as this was for a hackathon project and it had to be built fast. But still, I wanted to have users for future potential features such as statistics by tracking games played and number of correct answers.</p>
<p>Requiring sign-up with email for a demo project can be a barrier for some people, and I wanted to make it easy to start playing right away. Additionally, I wanted to test out the multiplayer functionality with friends during the development process. Unfortunately, <a target="_blank" href="https://github.com/supabase/gotrue/issues/68">anonymous sign-in</a> is not currently supported by <a target="_blank" href="https://github.com/supabase/gotrue">GoTrue</a>, the authentication server used by Supabase.</p>
<p>There is a <a target="_blank" href="https://github.com/supabase/gotrue/issues/68#issuecomment-794998982">workaround</a>, however, as you can disable email confirmations and assign users a custom email and password when first visiting the app. The <code>AuthService</code>, which has "anonymous sign-in", looks like this:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _emailKey = <span class="hljs-string">'email'</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _defaultPassword = <span class="hljs-string">'82eb32f2a3ef'</span>;

  <span class="hljs-keyword">final</span> GoTrueClient _auth;
  <span class="hljs-keyword">final</span> SharedPreferences _preferences;

  AuthService(<span class="hljs-keyword">this</span>._auth, <span class="hljs-keyword">this</span>._preferences);

  Future&lt;AuthResponse&gt; signIn() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_preferences.containsKey(_emailKey)) {
      <span class="hljs-keyword">return</span> _auth.signInWithPassword(
        email: _preferences.getString(_emailKey)!,
        password: _defaultPassword,
      );
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">final</span> email = _randomEmail;
      <span class="hljs-keyword">final</span> response =
          <span class="hljs-keyword">await</span> _auth.signUp(email: email, password: _defaultPassword);
      log(<span class="hljs-string">'User signed up with random email <span class="hljs-subst">$email</span>'</span>);
      _preferences.setString(_emailKey, email);
      <span class="hljs-keyword">return</span> response;
    }
  }

  <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> <span class="hljs-keyword">get</span> _randomEmail {
    <span class="hljs-keyword">return</span> <span class="hljs-string">'<span class="hljs-subst">${Uuid().v4().toString()}</span>@dartling.dev'</span>;
  }
}
</code></pre>
<p>On sign-in, a user is signed up with a random <code>@dartling.dev</code> email and a default password. The generated email is stored in shared preferences to avoid creating a new user every time on the same device. The <code>AuthService#signIn</code> function is called after a user selects their nickname.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673610295812/e2f4e131-8bbd-44df-a72c-1361bb57c038.png" alt="Nickname selection screen" class="image--center mx-auto" /></p>
<p>With users/authentication/authorization, I could also enable <a target="_blank" href="https://supabase.com/docs/guides/auth/row-level-security">Row Level Security</a> to restrict access to games and answers. As an example, here are the RLS policies for the <code>games</code> table:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Users can create games"</span> <span class="hljs-keyword">on</span> public.games
<span class="hljs-keyword">for</span> <span class="hljs-keyword">insert</span> <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (<span class="hljs-literal">true</span>);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Users can view games"</span> <span class="hljs-keyword">on</span> public.games
<span class="hljs-keyword">for</span> <span class="hljs-keyword">select</span> <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span> <span class="hljs-keyword">using</span> (<span class="hljs-literal">true</span>);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Games can only be updated by their hosts"</span> <span class="hljs-keyword">on</span> public.games
<span class="hljs-keyword">for</span> <span class="hljs-keyword">update</span> <span class="hljs-keyword">using</span> (auth.uid() = host_id)
<span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (auth.uid() = host_id);
</code></pre>
<h3 id="heading-game-codes">Game codes</h3>
<p>For the games to be multiplayer, someone would have to host a game and share it with others. Games can be shared with codes. For the game codes, I wanted to have strings that are short, easy to share between people, and unique. After some searching, I came across <a target="_blank" href="https://pub.dev/packages/hashids2">Hashids</a>, which fit my use case perfectly. With Hashids, you can map integers (which would be the game IDs in the database) to a unique hash of the length and characters of your choice.</p>
<p>Here are some snippets from the game code encoding and decoding logic.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Hashids config</span>
_hashIds = HashIds(
  minHashLength: <span class="hljs-number">4</span>,
  alphabet: <span class="hljs-string">'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'</span>,
);

<span class="hljs-comment">// game ID to game code</span>
<span class="hljs-built_in">String</span> _toGameCode(<span class="hljs-built_in">int</span> gameId) {
  <span class="hljs-keyword">return</span> _hashIds.encode(gameId);
}

<span class="hljs-comment">// game code to game ID</span>
<span class="hljs-built_in">int?</span> _toGameId(<span class="hljs-built_in">String</span> gameCode) {
  <span class="hljs-keyword">final</span> decoded =
      _hashIds.decode(gameCode.replaceAll(<span class="hljs-string">'O'</span>, <span class="hljs-string">'0'</span>).replaceAll(<span class="hljs-string">'l'</span>, <span class="hljs-string">'1'</span>));
  <span class="hljs-keyword">return</span> decoded.isNotEmpty ? decoded.first : <span class="hljs-keyword">null</span>;
}
</code></pre>
<p>I chose a minimum 4-character long hash and excluded the capital letter <code>O</code> and lowercase <code>l</code> from the alphabet, as I found them to look too similar to the numbers 0 and 1 respectively with the game's font. When users input a game code, <code>O</code> is mapped to <code>0</code> and <code>l</code> is mapped to <code>1</code>, to make things easier for everyone.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673626574300/f5fc8a84-b604-4011-ad89-4f2e89decfcb.png" alt="Screenshot showing the &quot;waiting screen&quot; of a game with the game code" class="image--center mx-auto" /></p>
<h3 id="heading-multiplayer-functionality">Multiplayer functionality</h3>
<p>By this point, I had an app with a simple UI, questions fetched from the Trivia API, plus Supabase set up with authentication. I also had logic to generate shareable game codes so that others can join someone's game. The last piece is the most fun one, multiplayer.</p>
<p>My plan to achieve multiplayer mode was simple; you can choose to host a game, and other players can join it. At any time, the host can choose to start the game, and each player answers the questions on their device until the end of the game.</p>
<p>With Supabase Realtime, we can have devices listen to any changes to a specific game in the games table. When a host starts the game, the game's status is updated from <code>pending</code> to <code>started</code>. Players in the game will be notified of the game's status update, and start the game. To enable listening to real-time updates of the games table, I had to make this small change:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">alter</span> publication supabase_realtime <span class="hljs-keyword">add</span> <span class="hljs-keyword">table</span> public.games;
</code></pre>
<p>With that enabled, data can be exposed as streams through the Supabase client.</p>
<pre><code class="lang-dart">Stream&lt;GameStatus&gt; getGameStatus(<span class="hljs-built_in">int</span> gameId) {
  <span class="hljs-keyword">return</span> _supabaseClient
      .from(<span class="hljs-string">'games'</span>)
      .stream(primaryKey: [<span class="hljs-string">'id'</span>])
      .eq(<span class="hljs-string">'id'</span>, gameId)
      .map((e) =&gt; GameStatus.fromString(e.first[<span class="hljs-string">'status'</span>]));
}
</code></pre>
<p>Initially, I planned to have an additional round/question number field in the games table, and have users listen to updates to the round field of a game so the game can move on to the next question at the same time for all players. Instead, I went for a hackier approach with a time limit. The host can choose how many seconds players will have to answer the question, and after the time is up, the game moves on to the next question.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673633191447/43b8e3ff-cd20-4dfc-af25-8f0db99f7849.png" alt="Screenshot showing a question with a time limit" class="image--center mx-auto" /></p>
<p>Once all questions are answered, the final scores are calculated and displayed to all players. To calculate the scores, I created a Postgres function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">function</span> calculate_scores(game_id_param <span class="hljs-built_in">int</span>) <span class="hljs-keyword">returns</span> <span class="hljs-keyword">table</span> (
  user_id <span class="hljs-keyword">uuid</span>, nickname <span class="hljs-built_in">text</span>, score <span class="hljs-built_in">int</span>
) <span class="hljs-keyword">as</span> $$ <span class="hljs-keyword">begin</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">query</span>
<span class="hljs-keyword">select</span>
  players.user_id :: <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">as</span> user_id,
  <span class="hljs-keyword">min</span>(players.nickname):: <span class="hljs-built_in">text</span> <span class="hljs-keyword">as</span> nickname,
  (<span class="hljs-keyword">count</span>(answers.id) * <span class="hljs-number">100</span>):: <span class="hljs-built_in">int</span> <span class="hljs-keyword">as</span> score
<span class="hljs-keyword">from</span>
  players
  <span class="hljs-keyword">join</span> questions <span class="hljs-keyword">on</span> questions.game_id = players.game_id
  <span class="hljs-keyword">left</span> <span class="hljs-keyword">join</span> answers <span class="hljs-keyword">on</span> answers.question_id = questions.id
  <span class="hljs-keyword">and</span> answers.user_id = players.user_id <span class="hljs-keyword">and</span> questions.correct_answer = answers.answer
<span class="hljs-keyword">where</span> players.game_id = game_id_param
<span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> players.user_id
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> score <span class="hljs-keyword">desc</span>;
<span class="hljs-keyword">end</span>;
$$ language plpgsql;
</code></pre>
<p>The <code>calculate_scores</code> function is called from the Supabase client:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-built_in">List</span>&lt;PlayerScore&gt;&gt; getScores(<span class="hljs-built_in">int</span> gameId) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">2</span>));
  <span class="hljs-keyword">final</span> scores = <span class="hljs-keyword">await</span> _supabaseClient.rpc(<span class="hljs-string">'calculate_scores'</span>,
      params: {<span class="hljs-string">'game_id_param'</span>: gameId}).select&lt;<span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;&gt;&gt;();
  log(<span class="hljs-string">'Player scores for game <span class="hljs-subst">$gameId</span>: <span class="hljs-subst">${scores.join(<span class="hljs-string">', '</span>)}</span>'</span>);
  <span class="hljs-keyword">return</span> scores.map(PlayerScore.fromJson).toList();
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673628367320/b1cd3bd1-8a60-432b-8e39-64fa8ac76b4b.png" alt="Screenshot of the results of a quiz game, with scores next to each player name" class="image--center mx-auto" /></p>
<p>And with that, the game is over! You get 100 points for every correct answer.</p>
<h2 id="heading-plans-for-the-future">Plans for the future</h2>
<p>Given more time, there are many more features I would have liked to implement for Supaquiz. The hackathon is over, but if I find myself still playing the game, I might spend some time improving it at some point.</p>
<ul>
<li><p>Player statistics (games played, games won, questions answered correctly...)</p>
</li>
<li><p>Quiz customization (categories, difficulty)</p>
</li>
<li><p>Other game modes (TV mode)</p>
</li>
</ul>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this post, I shared a few parts of my experience building Supaquiz, a real-time multiplayer quiz game with Flutter and Supabase. I went into some detail on some of the challenges faced, including the implementation of anonymous sign-in, game code generation and real-time functionality.</p>
<p>By the end of Launch Week 6, I finally had a working quiz game that I could play with friends. You can check out the live demo <a target="_blank" href="https://yallurium.github.io/supaquiz">here</a> and the source code <a target="_blank" href="https://github.com/yallurium/supaquiz">here</a>. I'm also thrilled to share that Supaquiz <a target="_blank" href="https://supabase.com/blog/launch-week-6-hackathon-winners">won the Best Flutter Project category</a> at the hackathon!</p>
<p>Overall, participating in the Supabase Launch Week 6 hackathon was a fun and rewarding experience. I'm looking forward to participating in more hackathons in the future.</p>
]]></content:encoded></item><item><title><![CDATA[Dev Retro 2022 - Another year of Flutter]]></title><description><![CDATA[Introduction
For me, 2022 was a year of growth and learning as I delved deeper into the world of Flutter development. In addition to building apps with Flutter, writing about Flutter has been a great way to share my knowledge and experiences with the...]]></description><link>https://dartling.dev/dev-retro-2022-another-year-of-flutter</link><guid isPermaLink="true">https://dartling.dev/dev-retro-2022-another-year-of-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[#DevRetro2022]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Fri, 06 Jan 2023 18:30:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1673030927828/e71b47e3-c407-4f11-83cb-1ec6a01ab4db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>For me, 2022 was a year of growth and learning as I delved deeper into the world of Flutter development. In addition to building apps with Flutter, writing about Flutter has been a great way to share my knowledge and experiences with the community.</p>
<p>In this blog post, I will share some personal highlights and accomplishments from the past year. I hope that, whether you're an experienced Flutter developer or just getting started, this post will inspire you to continue learning and growing with this incredible framework! So, let's dive in and take a look at my year with Flutter and the Dartling blog!</p>
<h2 id="heading-accomplishments">Accomplishments</h2>
<h3 id="heading-the-dartling-blog-grows">The Dartling blog grows</h3>
<p>In 2022, I published a total of <strong>11 blog posts</strong>. That's almost one blog post a month, which is very close to the goal I set for me. While I would have liked it to be more, it's more than I expected, and it's also <strong>550% more posts</strong> compared to 2021! I started the Dartling blog in May 2021, but only managed to publish 2 articles about <a target="_blank" href="https://dartling.dev/series/full-stack-flutter">using Flutter with Supabase</a>.</p>
<p>Here are the top 3 Dartling blog posts of 2022:</p>
<ul>
<li><p><a target="_blank" href="https://dartling.dev/displaying-a-loading-overlay-or-progress-hud-in-flutter">Displaying a loading overlay or progress HUD in Flutter</a></p>
</li>
<li><p><a target="_blank" href="https://dartling.dev/dynamic-theme-color-material-3-you-flutter">Dynamic theme color with Material 3 (You) in Flutter</a></p>
</li>
<li><p><a target="_blank" href="https://dartling.dev/how-i-built-and-published-a-flutter-app-in-48-hours">How I built and published a Flutter app in 48 hours</a></p>
</li>
</ul>
<p>And below are some stats for the year:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673000122181/105de102-9823-423a-a68f-7e9ccff0b010.png" alt="Dartling blog analytics for 2022" class="image--center mx-auto" /></p>
<p>If you're curious about the huge spike in views and visitors in September, that's the main topic of the next section.</p>
<h3 id="heading-4-articles-in-4-weeks">4 Articles in 4 Weeks</h3>
<p>Thanks to the <a target="_blank" href="https://townhall.hashnode.com/4-articles-in-4-weeks-hashnode-writeathon">4 Articles in 4 Weeks Hashnode Writeathon</a>, I was motivated enough to keep blogging consistently for a month, publishing one post per week. Participating in this writeathon was one of the highlights of my year in blogging, as it resulted in having two of my four articles written featured on Hashnode.</p>
<ul>
<li><p><a target="_blank" href="https://dartling.dev/how-i-built-and-published-a-flutter-app-in-48-hours">How I built and published a Flutter app in 48 hours</a></p>
</li>
<li><p><a target="_blank" href="https://dartling.dev/tips-and-tools-for-flutter-apps-in-production">Tips and tools for Flutter apps in production</a></p>
</li>
</ul>
<p>As a result of the articles being featured, Dartling became more visible and resulted in a huge amount of new followers. I started the year with less than 10 followers on Hashnode, and the blog is now at over 200! I would highly recommend joining any contests, hackathons or writeathons on Hashnode if you can.</p>
<h3 id="heading-growing-my-first-flutter-app">Growing my first Flutter app</h3>
<p>Two and a half years ago, I published my first ever mobile app built with Flutter, <a target="_blank" href="https://timelog.link/">Timelog</a>. Since then, I added a lot more features and made many improvements to it. Timelog has had almost 20k downloads in 2022, and that's only from the Play Store! Though it's not as popular on the App Store (yet), with just a little less than a thousand downloads total.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673009046832/763e4b78-a93b-4b5b-8394-af191daecbf2.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-publishing-my-second-flutter-app">Publishing my second Flutter app</h3>
<p>Because of the lockdown rules introduced during the pandemic in early 2022, I had a lot of free time in the evenings while at home. I spent a lot of time building a second Flutter app, <a target="_blank" href="https://flowmoneytracker.com/">Flow, an expense tracking app</a>. I share more details, from design to implementation to publishing, in <a target="_blank" href="https://dartling.dev/how-i-built-and-published-a-flutter-app-in-48-hours">this post</a>.</p>
<h3 id="heading-supabase-launch-week-6-hackathon">Supabase Launch Week 6 Hackathon</h3>
<p>During the <a target="_blank" href="https://supabase.com/blog/launch-week-6-hackathon">Supabase Launch Week 6 Hackathon</a>, I decided it was time to brush up on my Supabase skills and build something using Flutter and Supabase. By the end of the hackathon, I had built a real-time, multiplayer quiz game, Supaquiz, which you can <a target="_blank" href="https://yallurium.github.io/supaquiz">play here</a>. You can play on your own or host a game and play with friends. You can also check out the source code <a target="_blank" href="https://github.com/yallurium/supaquiz">here</a>.</p>
<p>Supaquiz <a target="_blank" href="https://supabase.com/blog/launch-week-6-hackathon-winners">won the best Flutter project category</a>! I am planning to share some more on how I built it in a separate blog post soon.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673009228253/39dea49c-c34a-49f9-a9ec-8c93c38e029d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-lessons-learned">Lessons learned</h2>
<h3 id="heading-stay-consistent">Stay consistent</h3>
<p>One of the biggest lessons I learned during the past year is the importance of staying consistent. For both writing and app development, I found that the key to making meaningful progress was to set a regular schedule and stick to it.</p>
<h3 id="heading-keep-up-to-date">Keep up-to-date</h3>
<p>Another important lesson I learned is the value of keeping up-to-date with the latest developments in the Flutter ecosystem. Flutter keeps growing, and there is always something new to learn and explore. Through <a target="_blank" href="https://flutternewsletter.volpato.dev/">newsletters</a>, <a target="_blank" href="https://www.reddit.com/r/FlutterDev/">Reddit</a> and Twitter, it's easy to stay informed about the latest features, new packages, or interesting projects built with Flutter.</p>
<h2 id="heading-goals-for-the-future">Goals for the future</h2>
<p>I have several goals that I hope to achieve in 2023. One of my main goals is to continue writing about Flutter on a (more) regular basis and contribute to the Flutter community. I plan to write at least two blog posts per month, sharing my experiences and insights with other Flutter developers, and improving my skills and knowledge.</p>
<p>In addition to writing, I also hope to continue growing my existing Flutter apps, adding new features (and maybe even doing some marketing) to make them even better. And perhaps I might even release a third Flutter app!</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>All in all, it's been a productive year, but as always there is room for improvement! Balancing writing, and building personal projects on top of a full-time job can be quite the challenge! However, I am excited to continue exploring Flutter and getting a stronger and deeper understanding of the framework.</p>
<p>If you have any particular topics or aspects of Flutter development you'd like to see more of in mind, please let me know in the comments! Do you have any plans or goals that have to do with writing or Flutter for the new year?</p>
<p>Keep building, writing, or both! Happy new year!</p>
<p>Cover image generated by OpenAI's <a target="_blank" href="https://openai.com/dall-e-2"><strong>DALL-E</strong></a>.</p>
]]></content:encoded></item><item><title><![CDATA[Toggle full screen mode in Flutter by controlling the status and button bars]]></title><description><![CDATA[Introduction
In this tutorial, we will show how we can enter, as well exit, a "full screen mode" in a Flutter application. This can be achieved by controlling the visibility of the native platform's overlays. In the case of mobile platforms such as A...]]></description><link>https://dartling.dev/toggle-full-screen-mode-in-flutter</link><guid isPermaLink="true">https://dartling.dev/toggle-full-screen-mode-in-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 01 Nov 2022 19:42:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1667238745743/k_Cu5a7LF.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial, we will show how we can enter, as well exit, a "full screen mode" in a Flutter application. This can be achieved by controlling the visibility of the native platform's overlays. In the case of mobile platforms such as Android and iOS, the overlays are the status bar at the top, and optionally the button or navigation bar at the bottom.</p>
<p>This solution does not require using any external packages, so this should be simple to do. Let's get started!</p>
<h2 id="heading-control-system-overlay-visibility">Control system overlay visibility</h2>
<p>As already mentioned, if we want our Flutter app to take the full screen, we need to hide any overlays by the native platform (system).</p>
<p>We can do this by using the static <code>setEnabledSystemUIMode</code> method provided by <code>SystemChrome</code>.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/services.dart'</span>; <span class="hljs-comment">// For `SystemChrome`</span>

<span class="hljs-keyword">void</span> enterFullScreen() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
}

<span class="hljs-keyword">void</span> exitFullScreen() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
}
</code></pre>
<p>That's it, actually! By setting the <code>SystemUiMode.manual</code> mode, we can specify which overlays should be visible. By passing an empty list, we can hide all overlays, and by providing all overlays, we can show them once again.</p>
<p>There are currently just two types of system UI overlays. One is the top overlay, typically the status bar, and the second is the bottom overlay, usually a button bar used for navigation. If we wanted, we could only hide just the top overlay, leaving the bottom button bar available for user interactions.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> enterFullScreenButKeepBottomOverlay() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom]);
}
</code></pre>
<h2 id="heading-types-of-full-screen-modes">Types of full-screen modes</h2>
<p>In the above example, we used the <code>SystemUiMode.manual</code> mode to achieve a full screen mode by hiding all overlays. But there are different values for <code>SystemUiMode</code> we  could be using instead. All 4 modes will hide all overlays, but behave differently.</p>
<h3 id="heading-manual-with-no-overlays">Manual with no overlays</h3>
<p>This is the approach we used in the above example. All overlays are hidden, and the overlays can only be visible again by programmatically showing them (e.g. using the <code>exitFullScreen()</code> method above). Swiping the edges of the display can briefly show the overlays, but they are automatically hidden after a couple of seconds.</p>
<h3 id="heading-lean-back">Lean back</h3>
<p>In <code>SystemUiMode.leanBack</code> mode, while the overlays are initially hidden, they are visible after tapping anywhere on the display. One thing I noticed, for Android 12 at least, is that this only shows the navigation bar at the bottom (if there is one) and not the status bar, so this could depend on the operating system or device. You can still show the status bar by swiping the edges of the display.</p>
<h3 id="heading-immersive">Immersive</h3>
<p><code>SystemUiMode.immersive</code> mode is similar to <code>leanBack</code> mode, but the overlays are only shown when swiping the edges of the display.</p>
<h3 id="heading-immersive-sticky">Immersive sticky</h3>
<p><code>SystemUiMode.immersiveSticky</code> is the same as <code>immersive</code>, but with one difference. The swipe gesture which triggers showing the overlays can be received by the application, in case you want to react on it.</p>
<h3 id="heading-edge-to-edge">Edge to edge</h3>
<p> In <code>SystemUiMode.edgeToEdge</code> mode, the overlays are still visible, but are actually rendered over the application. I tried this in an Android emulator, and the Flutter app's <code>AppBar</code> is shown below the status bar; it is not rendered on top of it. But the app's layout does seem to be affected when using this mode. This doesn't really achieve a full screen mode, but it's still good to know!</p>
<p>When enabling a system UI mode other than <code>manual</code>, we don't have to provide any overlays.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> enterFullScreen() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
}
</code></pre>
<h2 id="heading-listen-to-overlay-changes">Listen to overlay changes</h2>
<p>We can use <code>SystemChrome.setSystemUIChangeCallback</code> to register a callback to be called when system overlay visibility is changed. This could be useful, as in some modes the user can tap or swipe on the display, which could result to the overlays becoming visible again.</p>
<p>The callback has a <code>systemOverlaysAreVisible</code> boolean, and if its value is true, it means we are not in full screen mode. I noticed the value is not always what's expected though, so if you're planning on using it, make sure to test it out and see how it behaves.</p>
<p>To see it in action, we could register the callback in <code>initState</code> in a stateful widget.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible) <span class="hljs-keyword">async</span> {
    log(<span class="hljs-string">'System overlays are visible: <span class="hljs-subst">$systemOverlaysAreVisible</span>'</span>);
  });
}
</code></pre>
<h2 id="heading-usage-examples">Usage examples</h2>
<p>Now that we know how to toggle full screen mode in a Flutter app, let's apply it in practice. We will use the default counter app/template to get started.</p>
<pre><code>flutter create .
</code></pre><p>How our app utilizes full screen mode can depend on the use case. We might want to have "focus mode" which opens up in its own page, or we might want to toggle full screen mode on an existing page.</p>
<h3 id="heading-enter-a-new-page-in-full-screen-mode">Enter a new page in full-screen mode</h3>
<p>In the counter app, let's introduce a new focus mode page, which can be opened by a button on the main screen. The logic is identical to the main counter screen, but without the app bar.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// main.dart (_MyHomePageState)</span>
TextButton(
    onPressed: () {
    Navigator.push(context,
        MaterialPageRoute(builder: (_) =&gt; <span class="hljs-keyword">const</span> FocusModePage()));
    },
    child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Enter focus mode'</span>),
),
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// focus_mode.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FocusModePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> FocusModePage({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  State&lt;FocusModePage&gt; createState() =&gt; _FocusModePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_FocusModePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">FocusModePage</span>&gt; </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    log(<span class="hljs-string">'Entering full screen mode...'</span>);
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    <span class="hljs-keyword">super</span>.dispose();
    log(<span class="hljs-string">'Exiting full screen mode...'</span>);
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
        overlays: SystemUiOverlay.values);
  }

  <span class="hljs-keyword">void</span> _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            <span class="hljs-keyword">const</span> Text(
              <span class="hljs-string">'You have pushed the button this many times:'</span>,
            ),
            Text(
              <span class="hljs-string">'<span class="hljs-subst">$_counter</span>'</span>,
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: <span class="hljs-string">'Increment'</span>,
        child: <span class="hljs-keyword">const</span> Icon(Icons.add),
      ),
    );
  }
}
</code></pre>
<p>In this stateful widget, we enter full screen mode in <code>initState</code>, and exit it in <code>dispose</code>. This way, full screen mode is only enabled while the user is on this specific page.</p>
<h3 id="heading-toggle-full-screen-mode-in-a-page">Toggle full-screen mode in a page</h3>
<p>If we want to enable full screen mode in an existing page, we could toggle it with buttons. On the main counter page, let's add two new buttons, conditional on whether full screen is enabled.</p>
<p>We add a variable named <code>_isFullScreen</code> to keep track of whether we enabled full screen mode or not.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// main.dart (_MyHomePageState)</span>
<span class="hljs-built_in">bool</span> _isFullScreen = <span class="hljs-keyword">false</span>;
</code></pre>
<p>Depending on the value of <code>_isFullScreen</code>, we either show the button to enter full screen mode, or exit it.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// main.dart (_MyHomePageState)</span>
<span class="hljs-keyword">if</span> (!_isFullScreen)
  TextButton(
    onPressed: () {
      log(<span class="hljs-string">'Entering full screen mode...'</span>);
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
      setState(() {
        _isFullScreen = <span class="hljs-keyword">true</span>;
      });
    },
    child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Enter full screen'</span>),
  ),
<span class="hljs-keyword">if</span> (_isFullScreen)
  TextButton(
    onPressed: () {
      log(<span class="hljs-string">'Exiting full screen mode...'</span>);
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
          overlays: SystemUiOverlay.values);
      setState(() {
        _isFullScreen = <span class="hljs-keyword">false</span>;
      });
    },
    child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Exit full screen'</span>),
  ),
</code></pre>
<p>This might become a bit more complicated if the gestures by a user cause the overlays to become visible as from the widget/state's point of view full screen is still enabled. For that, we could make use of <code>SystemChrome.setSystemUIChangeCallback</code> to track any changes.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we showed how to enter and exit full screen mode in a Flutter app without using any external packages. We also showed how we would use this functionality by either having certain full screen pages or enabling this mode manually on a page.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/full_screen_mode">here</a>.</p>
<p>If you found this helpful and would like to be notified of future tutorials, please subscribe to the newsletter with your email below!</p>
]]></content:encoded></item><item><title><![CDATA[Tips and tools for Flutter apps in production]]></title><description><![CDATA[Introduction
Publishing your first app feels great. Flutter makes it very easy to build apps on any platform, so it's easy to turn your idea into a production-ready app and get it published on the Play Store, App Store, or anywhere.
But what should y...]]></description><link>https://dartling.dev/tips-and-tools-for-flutter-apps-in-production</link><guid isPermaLink="true">https://dartling.dev/tips-and-tools-for-flutter-apps-in-production</guid><category><![CDATA[Flutter]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[#week4]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Thu, 08 Sep 2022 15:07:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662622049195/frUNdtex9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Publishing your first app feels great. Flutter makes it very easy to build apps on any platform, so it's easy to turn your idea into a production-ready app and get it published on the Play Store, App Store, or anywhere.</p>
<p>But what should you do next, if you want to keep working on the app? My first instinct, personally, would be to just build more features! This can be a good idea, of course, especially if you've built a relatively simple app and want to keep improving it and introducing new, more advanced features, which can also help your app stand out from the rest. However, once your app is live, there are a few other things you should focus on first, or at least in parallel, while you're working on new features.</p>
<p>People build apps for many reasons, but most commonly it's to solve a problem and make people's lives easier. In this article, I will share some tips and things you can do once you've published an app in production to grow your app and make it even better. I will focus more on apps built with Flutter, and share tools, packages and services you can use, some which are specific to Flutter, and some not.</p>
<h1 id="heading-improve-your-process">Improve your process</h1>
<h2 id="heading-continuous-delivery">Continuous delivery</h2>
<p>If you're publishing your app on multiple stores and platforms, or even just one, the process can be quite time consuming. Even if it takes a few minutes at a time, the process can be very manual and repetitive.</p>
<p>There are many tools you can use to automate releases and deployments of your app, and this <a target="_blank" href="https://docs.flutter.dev/deployment/cd">official Flutter guide</a> lists some of the options available to you.</p>
<p>There are all-in-one tools such as <a target="_blank" href="https://codemagic.io">Codemagic</a> which provide both CI (Continuous Integration) and CD (Continuous Delivery), so you could run tests and some verifications before your releases. If you have existing CI workflows, such as with GitHub Actions or GitLab, you can integrate <a target="_blank" href="https://docs.fastlane.tools">fastlane</a> with them to do this.</p>
<p>CI/CD can save you a lot of time, especially if you start releasing more often. While you don't really have to set this up before publishing the first version of your app, it's a good idea to set it up as early as possible. The setup will only take a little bit of time, but will end up saving you a lot more time in the future.</p>
<h2 id="heading-screenshot-creation">Screenshot creation</h2>
<p>On stores such as the Play Store and App Store, app listings are required to have  screenshots of your app. Taking these can be very tedious, as you might have to do it for multiple platforms, and even for multiple screen sizes!</p>
<p>Screenshots are important to get right, as it's one of the first things people will see, and you want them to want to download your app after checking out the screenshots. You can use tools such as <a target="_blank" href="https://appscreens.com">AppScreens</a> which can help with the layouts and adding phone frames or text in the screenshots. It works for both the Play Store and App Store, in addition to multiple screen size support and localization, so all you have to do is set it up once and then upload the "raw" screenshots.</p>
<p>Even with screenshot creator tools, you still need to manually capture these screenshots of your app. This is usually not too bad, as your app's main look might not change that often. But this can still be very time consuming and repetitive, as you need to repeat the same process for all supported platforms as well as screen sizes. You might also want to have some dummy data in the app so that the screenshots are not of an "empty" app. The screenshot taking process can also be automated. The tool <a target="_blank" href="https://docs.fastlane.tools/actions/screengrab">screengrab</a> is included with fastlane and can help capture screenshots in multiple languages on emulators or real devices, but does not currently support Flutter. There is an <a target="_blank" href="https://medium.com/@nocnoc/automated-screenshots-for-flutter-f78be70cd5fd">article</a> on how to achieve this in Flutter with the <a target="_blank" href="https://pub.dev/packages/screenshots">screenshots</a> package, but it seems to be outdated by now. However, it's worth spending at least some time to at least make this process easier and shorter, if not completely automated, as it could literally save you hours; I did this manually for apps I have on both the Play Store and App Store and the whole process usually took over an hour.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662541316805/MRCmmkVLF.png" alt="A blue bird holding a camera" class="image--center mx-auto" /></p>
<h2 id="heading-crash-reporting">Crash reporting</h2>
<p>If the app crashes for your users, it would be good to know about it before they leave a bad review or feedback! You can use a service such as <a target="_blank" href="https://sentry.io">Sentry</a> which is easy to integrate with their Flutter SDK to track any errors, monitor them, and act on them.</p>
<h2 id="heading-analytics">Analytics</h2>
<p>Besides crashes, it's also a good idea to know how users are using your app. You can use services such as <a target="_blank" href="https://amplitude.com">Amplitude</a>, which integrate with Flutter, to track which features are being used and how. This can help you figure out what to work on next, or what to improve. And if some features are not being used as much as you expected, maybe you could consider removing them, or changing them.</p>
<h1 id="heading-grow-your-app">Grow your app</h1>
<h2 id="heading-ask-for-reviews">Ask for reviews</h2>
<p>Once you're happy with your app and are confident it is bug-free, it's a good idea to ask for reviews inside the app. You can use packages such as <a target="_blank" href="https://pub.dev/packages/in_app_review">in_app_review</a> that will show the native Store review pop-ups to your users, and you can decide to only show this once a user has been using the app for some time or has used some specific features an arbitrary number of times.</p>
<p>This is a great, easy way to get more reviews, and more (ideally positive) reviews can also mean more downloads.</p>
<h2 id="heading-ask-for-feedback">Ask for feedback</h2>
<p>While users usually leave feedback in reviews, it's also good to have an option for people to message you with any feedback or questions they have. Usually this through email. With the <a target="_blank" href="https://pub.dev/packages/url_launcher">url_launcher</a> plugin you can have users open their email app straight to your feedback email. This could be a personal email or a feedback-specific email for your app's domain (e.g. <code>support@myflutterapp.com</code>). For custom domains, if you don't want to pay for email services, you can use an email forwarding service such as <a target="_blank" href="https://app.improvmx.com">ImprovMX</a> to forward any feedback to your support email to your personal one.</p>
<p>Gathering feedback from users is a great way to get to know how users use your app, in addition to analytics. You can also get suggestions on things to improve on or features to add. And as a tip, if you receive any emails with very positive feedback, make sure to ask them to leave a review or rating in the Store if they haven't already!</p>
<h2 id="heading-monetization">Monetization</h2>
<p>Perhaps not great for your users, but great for you! Monetizing your app gives you a good incentive to keep working on it and improving it. Ads can usually be bad for the user experience, so in-app purchases to unlock more advanced, "premium" features would be a good approach. For apps that require a server/backend to store data, subscriptions would be a good fit as they can also cover server costs. If your app is offline-only, maybe a one-time-purchase makes more sense. Services such as <a target="_blank" href="https://revenuecat.com">RevenueCat</a> have Flutter SDKs and make it easy to implement in-app purchases, but you can also use Google's official <a target="_blank" href="https://pub.dev/packages/in_app_purchase">in_app_purchase</a> plugin, though for the latter you will need a backend to validate receipts.</p>
<p>Locking functionality behind a paywall can also help you validate how useful your app is. If people pay to use your app's more advanced features, it means they found your app helpful and have probably been using it for a while.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662541347891/emg1RjVVw.png" alt="A blue bird putting a coin in a piggy bank" class="image--center mx-auto" /></p>
<h2 id="heading-localization">Localization</h2>
<p>Even with your app just being in the English language, you can still get users from all over the world. However, by localizing your app you can expand your target audience for little cost, as a lot of mobile phone users prefer to use apps in their native language. You just need the translations, and <a target="_blank" href="https://docs.flutter.dev/development/accessibility-and-localization/internationalization">localization in Flutter</a> is very easy to set up.</p>
<p>At the very least, you can localize your app store listings (description, features), to make your app easier to find in app stores of different countries.</p>
<h2 id="heading-accessibility">Accessibility</h2>
<p>Besides localization, another way to reach more users is to improve your app's accessibility. Flutter has an <a target="_blank" href="https://docs.flutter.dev/development/accessibility-and-localization/accessibility">accessibility page</a> describing how you can test your app's accessibility and what you can do to address any potential issues, and there are also <a target="_blank" href="https://medium.com/flutter-community/a-deep-dive-into-flutters-accessibility-widgets-eb0ef9455bc">accessibility widgets</a> you can use to improve accessibility.</p>
<h2 id="heading-app-store-optimization">App store optimization</h2>
<p>App store optimization, typically referred to as ASO, is important if your app is on stores such as the Play Store and App Store. The more you optimize, the more people can find your app while browsing the stores, and the more downloads and users you get. I will not go into detail about this here as there are already countless resources for ASO out there. Two points already mentioned above, localization and asking for reviews, are good ways to improve your app store ranking.</p>
<h1 id="heading-keep-your-users">Keep your users</h1>
<h2 id="heading-on-boarding">On-boarding</h2>
<p>Simply getting people to download your app is never enough. Most people are usually testing out different apps, so it's very likely they only briefly install your app, decide it's not what they're looking for and then uninstall it. This is why a user's first interaction with your app is important.</p>
<p>Introducing an on-boarding flow is an easy way to highlight your app's features when an app is first opened. With Flutter, you can quickly build an on-boarding flow with multiple pages to do this. One example would be to use the <a target="_blank" href="https://pub.dev/packages/smooth_page_indicator">smooth_page_indicator</a> package as well as some images (<a target="_blank" href="https://undraw.co/">unDraw</a> is a good free resource) with descriptions (ideally, localized!) of your app's main or stand-out features.</p>
<h2 id="heading-tutorial">Tutorial</h2>
<p>Regardless of how complex your app is, it could be helpful to show  users how to use the app. A tutorial, also called a tour or product tour, is usually done by highlighting the buttons or areas users should be tapping or looking at. There are a lot of <a target="_blank" href="https://pub.dev/packages?q=tutorial">tutorial packages</a> for Flutter on <a target="_blank" href="https://pub.dev">pub.dev</a> that make it easy to highlight widgets and display descriptions.</p>
<p>The tutorial is a good follow-up to the initial on-boarding, and it's a good way to get users to interact more with your app and use its main functionality right from the beginning.</p>
<h2 id="heading-hook">Hook</h2>
<p>After a good on-boarding flow and tutorial, people are more likely to continue using your app! But how do you get them to keep using your app? This can vary depending on the type of app, but a user might install your app, find it useful, but then forget to use it regularly.</p>
<p>You can "nudge" your users by sending a daily <a target="_blank" href="https://pub.dev/packages/flutter_local_notifications">local notification</a> or push notification at a good time to get them to use your app. You can also incentivize continuous activity by introducing gamification or showing charts and streaks of the user's recent activity.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662541435251/7X2aLFLov.png" alt="A blue bird pointing at a hook with its beak" class="image--center mx-auto" /></p>
<h1 id="heading-spread-the-word">Spread the word</h1>
<h2 id="heading-landing-page">Landing page</h2>
<p>You might already have a domain for the app, and some "legal" pages (privacy policy, terms and conditions). But do you have a landing page for the app? It doesn't have to be anything fancy, but it's useful to have a simple page with buttons to the app stores where you list the main features of the app. If you already have good reviews on the stores, you could also list some of them in this page; user testimonials are a good way to convince people to at least try out your app.</p>
<p>People can stumble across your landing page either by searching for your app or by accident, or by someone sharing the landing page. This is good if your app is on multiple platforms, as you have a main page where users can see all available platforms, head to the correct store or website and download your app.</p>
<p>For this tip, using Flutter is actually not recommended! SEO matters for the landing page, so a Flutter web app would not help at all with this.</p>
<h2 id="heading-share-your-app">Share your app</h2>
<p>If you've built something you think others will find useful, you should share it! Having a landing page, as mentioned above, helps with this as you can promote your app by simply sharing this landing page. So, tell your friends, Twitter, Reddit... everyone!</p>
<p>You can also build share functionality in your app with the <a target="_blank" href="https://pub.dev/packages/share_plus">share_plus</a> package, so that users who like your app can share it with friends.</p>
<h2 id="heading-get-featured">Get featured</h2>
<p>You can share your app with press, such as journalists which write about apps on different websites and blogs. Getting featured on such a site could be a great way to get downloads. You can simply reach out to sites or specific people by sharing your landing page and some info, but in this case, having a "press kit" including a description and details of your app's features, screenshots and/or videos, will make it more likely to actually convince people to write about your app. Using a service such as <a target="_blank" href="https://impresskit.net">ImpressKit</a> can help with setting up a press kit.</p>
<p>It's also possible to get featured on app stores such the Google Play Store or Apple App Store. For Apple's App Store, you can actually <a target="_blank" href="https://developer.apple.com/app-store/getting-featured">ask to get featured</a>, and the team there will review your request. And if your request is rejected, keep asking! You can ask to be featured for every new/significant update of your app.</p>
<h2 id="heading-launch">Launch</h2>
<p>Another way to spread the word about your app and get more users is by "launching" your app on different places. ProductHunt is a popular site for launching products, and though again it can depend on your app if launching there will get you any visibility and downloads, showcasing your app on multiple sites is still useful. Every download counts!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662541403954/1wfTibrBM.png" alt="A blue bird flying in space" class="image--center mx-auto" /></p>
<h1 id="heading-wrapping-up">Wrapping up</h1>
<p>In this article, we listed some tips on things you can do after you publish your Flutter app, from improving the development and deployment process, to getting more downloads, and to getting users to keep using your app.</p>
<p>Some of these are actually never-ending processes (if you want them to be!), as there are always things to improve on and optimize, in addition to new features. But these are highly recommended if you want to keep working on your app.</p>
<p>Images generated by OpenAI's <a target="_blank" href="https://openai.com/dall-e-2">DALL-E</a>.</p>
<p>This article was published as the #week4 article for #4articles4weeks.</p>
]]></content:encoded></item><item><title><![CDATA[How I built and published a Flutter app in 48 hours]]></title><description><![CDATA[Introduction
A couple of years ago, I built and published my first app built with Flutter, Timelog. It's a time tracking app which lets you track your time across different activities, and set daily/weekly/monthly goals per activity. I built this app...]]></description><link>https://dartling.dev/how-i-built-and-published-a-flutter-app-in-48-hours</link><guid isPermaLink="true">https://dartling.dev/how-i-built-and-published-a-flutter-app-in-48-hours</guid><category><![CDATA[Flutter]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[#week3]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Sat, 03 Sep 2022 10:48:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662201907858/kxZQ_wOdY.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>A couple of years ago, I built and published my first app built with Flutter, <a target="_blank" href="https://timelog.link">Timelog</a>. It's a time tracking app which lets you track your time across different activities, and set daily/weekly/monthly goals per activity. I built this app mainly for myself, but I was quite happy to find out that a lot of other people have found it useful too.</p>
<p>Fast forward to the start of this year, and I was looking forward to building another app with Flutter. I decided to build <a target="_blank" href="https://flowmoneytracker.com">Flow</a>, a simple expense tracking app that would fit my needs, as I found the tons of expense tracking apps already out would either have clunky UIs or way too many features that I didn't need, yet still not flexible enough.</p>
<p>Firstly, I'd like to point out that no, this is not a guide to build and publish an app in production in a weekend. The title may be a little misleading (sorry!), but by 48 hours I mean 48 total hours of development of the app as well as the landing page, in addition to the Play Store listing (I haven't got to publishing this to the App Store yet). This process actually took me about 3 months in total, and I was mostly building this during the weekends and evenings in 1-3 hour chunks.</p>
<p>I actually used my first Flutter app, Timelog, to track how much time I spent building Flow, and that's how I noticed that by the time I had published Flow to the Play Store, I was at roughly 48 hours tracked in total. I also ended up looking at Timelog's source code to see how I did some things, and even copied some approaches and technologies used.</p>
<p>In this post, I will go over the process I took to build Flow, an expense tracking app with Flutter, in 48 hours of total development. We will start from the (a bit too short) planning phase, followed by the implementation, and then the final steps leading to publishing the app on the Play Store.</p>
<h1 id="heading-planning">Planning</h1>
<p>This phase was relatively short, and mostly consisted of thinking about what features I would want such an app to have, at least for a minimum viable product.</p>
<h2 id="heading-features">Features</h2>
<h3 id="heading-expense-tracking">Expense tracking</h3>
<p>This was an expense tracking app, so of course it would need to support recording your expenses: an amount, a date, and a category. Users should be able to see a history of their expenses, as well as manage their categories.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197310444/WvZrU8qJS.png" alt="History of expenses in Flow" class="image--center mx-auto" /></p>
<h3 id="heading-statistics">Statistics</h3>
<p>Of course, the reason you would track your expenses is to monitor your spending and see how much you spend and on what. One of the main features would be to display useful charts that would show you total amount spent per category, with options to filter categories and set specific time ranges.</p>
<p>For this, I used <a target="_blank" href="https://pub.dev/packages/syncfusion_flutter_charts">syncfusion_flutter_charts</a>. It's very customizable and has every chart I could think of, or at least needed. I had also used the same library previously for Timelog, so this was an easy choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197484103/dsVfXzob0.png" alt="Stats of your expenses in Flow" class="image--center mx-auto" /></p>
<h3 id="heading-labels">Labels</h3>
<p>For Timelog, my first app, the stand-out feature which most of the existing time trackers did not have were goals you can set per activity. For example, I have a reading goal of 2 hours per week, and a blogging goal of 4 hours per month (though thanks to <a target="_blank" href="https://townhall.hashnode.com/4-articles-in-4-weeks-hashnode-writeathon#heading-week-2">#4articles4weeks</a>, I'm already well past over 4 this month!).</p>
<p>For Flow, the stand-out feature is the ability to assign labels on each expense. For example, you could have labels specific to locations, trips, or people. This way, you could track exactly how much you spend e.g. on a trip, without having to assign the generic "Travel" category to every expense.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197510768/h9nteKhiB.png" alt="Assigning labels to an expense in Flow" class="image--center mx-auto" /></p>
<h3 id="heading-currencies">Currencies</h3>
<p>Users in different countries will need to track expenses for different currencies. The user should be able to pick their own currency, and <a target="_blank" href="https://pub.dev/packages/currency_picker">this package</a> made this very easy to implement. Supporting multiple currencies at the same time would make things more complicated, so this was left out and added to the backlog as a feature for the future.</p>
<h2 id="heading-design">Design</h2>
<p>I'm not a designer, but thankfully, it's easy to build beautiful looking UIs with Flutter. I decided to simply use the Material widgets to make my life easier, as the Material look is decent enough for all platforms.</p>
<p>The main layout is just a <code>Scaffold</code> with a <code>NavigationBar</code> at the bottom and a <code>FloatingActionButton</code> to record expenses. There's also a lot of <code>ListTile</code> widgets used for the expense history, settings, and the category/label management pages, as well as icons and charts. The most complex widget that I built from scratch was the number picker, which you can input the expense amount with. It's really just a bunch of rows and columns, but the decimal button logic was a bit tricky.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197534802/NsY2JB7Nv.png" alt="The number picker widget in Flow" class="image--center mx-auto" /></p>
<h1 id="heading-implementation">Implementation</h1>
<h2 id="heading-data-storage">Data storage</h2>
<p>For an expense tracking app, we need to store the user's expenses and categories. At least for the first version, we're just going to store the data locally on the user's device. For this, I went with <a target="_blank" href="https://drift.simonbinder.eu/">Drift</a>, which is built on top of <code>sqflite</code>. For less critical data such as the theme and user preferences, <a target="_blank" href="https://pub.dev/packages/shared_preferences">shared_preferences</a> is used instead.</p>
<h2 id="heading-state-management">State management</h2>
<p>I spent ages trying to figure out which state management approach to use for my first app, so thankfully this time it was easy. For state management, I used "vanilla" Flutter; a combination of <code>FutureBuilder</code>s, <code>StreamBuilder</code>s, <code>ValueNotifier</code>/<code>ValueListenableBuilder</code>s, and even <code>StatefulWidget</code>s.</p>
<p>Drift, the persistence library supports exposing data not just as futures but also as streams, which emit new events every time the data changes. Most views showing data such as expenses, categories or labels simply use a <code>StreamBuilder</code> to display the data, and these are refreshed automatically every time something is added or changed.</p>
<p>Other views that require "complex" state such as the expense creation page, are made of small <code>StatefulWidget</code>s that communicate changes to the data through callback methods, and state changes that influence other widgets can do so through <code>ValueNotifier</code>s.</p>
<p>In total, so far, the app uses 12 <code>StreamBuilder</code>s, 8 <code>FutureBuilder</code>s, and 9 <code>ValueListenableBuilder</code>s. No external libraries!</p>
<h2 id="heading-architecture">Architecture</h2>
<p>Flow is a simple app so far. A lot of the business/data logic that deals with creating expenses, storing user settings and configuring notifications is isolated in specific <code>Service</code> and <code>Repository</code> classes. These classes are passed around to widgets through the use of a <code>Services</code> inherited widget, which holds the references to instances of any dependencies needed.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Services</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">InheritedWidget</span> </span>{
  <span class="hljs-keyword">final</span> ExpenseRepository expenseRepository;
  <span class="hljs-keyword">final</span> SettingsService settingsService;
  <span class="hljs-keyword">final</span> NotificationService notificationService;
  <span class="hljs-keyword">final</span> AnalyticsService analyticsService;
  <span class="hljs-keyword">final</span> SubscriptionService subscriptionService;

  Services({
    Key? key,
    ...
    <span class="hljs-keyword">required</span> Widget child,
  })  : ...
        <span class="hljs-keyword">super</span>(key: key, child: child);

  <span class="hljs-keyword">static</span> Services of(BuildContext context) {
    <span class="hljs-keyword">final</span> Services? result =
        context.dependOnInheritedWidgetOfExactType&lt;Services&gt;();
    <span class="hljs-keyword">assert</span>(result != <span class="hljs-keyword">null</span>, <span class="hljs-string">'No Services found in context'</span>);
    <span class="hljs-keyword">return</span> result!;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> updateShouldNotify(Services oldWidget) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
  }
}
</code></pre>
<p>To avoid coupling widgets with business logic, some complex bigger widgets/views have their own <code>ViewModel</code> class, usually per page/screen. This view model has all the dependencies needed (services or repositories), and the view simply calls methods on this view model.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewExpenseViewModel</span> </span>{
  <span class="hljs-keyword">final</span> ExpenseRepository _expenseRepository;
  <span class="hljs-keyword">final</span> CategoryRepository _categoryRepository;
  <span class="hljs-keyword">final</span> AnalyticsService _analyticsService;

  <span class="hljs-built_in">int</span> amount;
  <span class="hljs-built_in">DateTime</span> date;
  TimeOfDay? time;
  Category category;

  NewExpenseViewModel(
    <span class="hljs-keyword">this</span>._expenseRepository,
    <span class="hljs-keyword">this</span>._categoryRepository,
    <span class="hljs-keyword">this</span>._analyticsService, {
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.amount,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.category,
    <span class="hljs-keyword">this</span>.time,
  }) : date = <span class="hljs-built_in">DateTime</span>.now().startOfDay;

  Future&lt;<span class="hljs-keyword">void</span>&gt; save() <span class="hljs-keyword">async</span> {
    _analyticsService.track(<span class="hljs-string">'new_expense'</span>);
    <span class="hljs-keyword">await</span> _expenseRepository.create(
        amount, date, time, description, category.id, labels);
  }

  Future&lt;<span class="hljs-built_in">List</span>&lt;Category&gt;&gt; <span class="hljs-keyword">get</span> categories =&gt; _categoryRepository.all;
}
</code></pre>
<p>These views and view models are created by first getting the <code>Services</code> from the context in order to create it with any of its dependencies, like below:</p>
<pre><code class="lang-dart">Future&lt;Expense?&gt; openNewExpensePage(
    BuildContext context, <span class="hljs-built_in">int</span> amount, Category category) {
  <span class="hljs-keyword">final</span> services = Services.of(context);
  <span class="hljs-keyword">final</span> viewModel = NewExpenseViewModel(
    services.expenseRepository,
    services.categoryRepository,
    services.analyticsService,
    amount: amount,
    category: category,
  );
  <span class="hljs-keyword">return</span> Navigator.push&lt;Expense&gt;(
    context,
    MaterialPageRoute(
        builder: (_) =&gt; NewExpenseView(viewModel)),
  );
}
</code></pre>
<p>This is a basic architecture that's been working for me. It's quite simple and has no dependencies on external libraries, and view models can easily be tested in isolation. It does feel like a little bit of boilerplate at times, though, especially when trying to build something quickly, and especially in a team of one. I do occasionally "cheat" by accessing the services and their methods directly in widgets, though!</p>
<h2 id="heading-local-notifications">Local notifications</h2>
<p>Flow has a simple daily notification to remind you to register your expenses. This is set when you first open the app and can be toggled on/off and its time can be changed. For this, I used the <a target="_blank" href="https://pub.dev/packages/flutter_local_notifications">local_notifications</a> package.</p>
<h2 id="heading-theming">Theming</h2>
<p>No app is complete without dark mode! In Flutter, it's so simple to implement that it was one of the first things I worked on. I also used <a target="_blank" href="https://pub.dev/packages/flex_color_scheme">flex_color_scheme</a> with the Material 3 settings enabled to make my life a bit easier, and even have <a target="_blank" href="https://dartling.dev/implementing-true-black-dark-theme-mode-in-flutter">true black mode</a> out-of-the-box in addition to dark mode.</p>
<p>Since Flow is meant to be a simple expense tracking app, I also made use of Material 3's <a target="_blank" href="https://dartling.dev/dynamic-theme-color-material-3-you-flutter">dynamic color</a> feature to change the app's theme/color based on the user's main theme/color on their device.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197557246/yN4E1Ph1u.png" alt="True black mode in Flow" class="image--center mx-auto" /></p>
<h2 id="heading-monitoring">Monitoring</h2>
<p>While monitoring of any kind is optional, it's something I'd recommend to always have, just to have an idea of how your app is used, or how it doesn't work!</p>
<h3 id="heading-analytics">Analytics</h3>
<p>I find it useful to track metrics to measure how and how much the app is used and what features are used the most. Things like expenses created, new labels created, and which statistics/charts are viewed the most. This can help decide which features to focus on while working more on the app.</p>
<p>For Flow, I decided to go with Mixpanel, as it had an official Flutter package. However, I later realized that you can only have up to 5 reports on your dashboards on the free plan, which is far too little to get any useful insights. Because of this, my suggestion would be to go with something like <a target="_blank" href="https://www.docs.developers.amplitude.com/data/sdks/flutter">Amplitude</a> instead, which has a much more generous free tier.</p>
<h3 id="heading-crash-reporting">Crash reporting</h3>
<p>I went with <a target="_blank" href="https://docs.sentry.io/platforms/flutter/">Sentry</a> for error/crash reporting, which also has good support for Flutter. It's nice to be able to see any errors thrown while other users are using your app, as your tests might not catch everything. And in my case, I was only mostly testing things manually (sorry, TDD folks!).</p>
<h2 id="heading-on-boarding">On-boarding</h2>
<p>Another "nice-to-have" is an on-boarding flow. This is what is shown when a user first starts up the app. I used the <a target="_blank" href="https://pub.dev/packages/smooth_page_indicator">smooth_page_indicator</a> package as well as some images from <a target="_blank" href="https://undraw.co/">unDraw</a> to show a few pages explaining Flow's core functionality. On the last page, the user can also opt in or out of the analytics and crash reporting mentioned above.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662197574864/euw1BARju.png" alt="On-boarding flow in Flow" class="image--center mx-auto" /></p>
<p>A good follow-up for the introductory pages would be a tutorial, or tour, of the main functionality. For Flow, for example, the FAB that lets you add expenses could be highlighted with a tool-tip explaining that this is where you need to tap to start tracking expenses. Flow is currently not that complicated to require such a tutorial, though.</p>
<h2 id="heading-in-app-purchases">In-app purchases</h2>
<p>I wanted to monetize the app from the beginning. Again an optional step, but it's nice to be able to validate if your app is actually good and unique enough for people to want to pay to use it more. I'm not a fan of ads at all, so I decided to implement a one-time "Flow Plus" purchase, which gives you access to more colors and icons for categories and labels, as well as unlimited labels.</p>
<p>For the implementation of in-app purchases, I used <a target="_blank" href="https://www.revenuecat.com/platform/flutter-in-app-purchases">RevenueCat</a>. When I was building Timelog, Flutter's official <a target="_blank" href="https://pub.dev/packages/in_app_purchase">in_app_purchase</a> plugin was not complete. This is how I found out about RevenueCat and its <a target="_blank" href="https://pub.dev/packages/purchases_flutter">purchases_flutter</a> package. RevenueCat has lots of extra features and functionality you might otherwise have to implement yourself, in addition to a generous free tier!</p>
<h2 id="heading-necessary-things">Necessary things</h2>
<p>In order to publish the app on the stores, there are some small things I had to include, namely, terms of use and privacy policy pages. I had already bought a domain for the app, <a target="_blank" href="https://flowmoneytracker.com">flowmoneytracker.com</a>, so I created two pages with the necessary information there, making sure to include third party services used for monitoring. I also spent some time on a very basic landing page.</p>
<p>Since I implemented in-app purchases, I also needed to make sure a "Restore purchases" option is available. This is a requirement for iOS apps, and I made sure to add this in the settings page as soon as possible, as it's one of the reasons Timelog's initial App Store submission was rejected!</p>
<h2 id="heading-a-few-more-things">A few more things</h2>
<p>I wasn't really in a rush to release Flow, which is why I spent some time to also implement some features that are not really necessary to have in a minimum viable product. Some of these I also wanted for myself and did not take too much time, so I went for them:</p>
<ul>
<li>The option to change the start day of the week (for the weekly statistics)</li>
<li>A set of preset categories as well as pages to manage categories and labels</li>
<li>Backup and restore functionality (useful for me in case I broke things while testing on my device! I had already been tracking my expenses while developing the app, and didn't want to lose my data)</li>
<li>Sharing functionality that simply shares the landing page's URL, and a feedback option that opens up an email to Flow's support email. I used <a target="_blank" href="https://improvmx.com/">ImprovMX</a> to forward support emails to a personal email account.</li>
</ul>
<h1 id="heading-production">Production</h1>
<h2 id="heading-publishing">Publishing</h2>
<p>And so, after a little less than 48 hours divided across 1-3 hour sessions, Flow was ready for production. I spent some time making screenshots for the Play Store listing, and quickly wrote short and long descriptions. A couple of days later, Flow was approved, and <a target="_blank" href="https://play.google.com/store/apps/details?id=com.yallurium.flow">live on the Play Store</a>.</p>
<p>Once I gather some more feedback from the Android version and make some more improvements, I also want to publish Flow to the App Store. Maybe even on desktop platforms, but I would need to make some layout changes first so that it looks good on larger screens.</p>
<h2 id="heading-future-plans">Future plans</h2>
<p>There are too many features an expense tracker like Flow could eventually support: recurring expenses, income tracking, budgeting, multiple accounts, multiple currencies... The current plan is to keep things simple and see if the labels feature actually makes Flow stand out from the hundreds of expense tracking apps out there. I do need to first market it a little, as currently it's just quietly sitting in the Play Store and not very easy to find!</p>
<h1 id="heading-wrapping-up">Wrapping up</h1>
<p>I hope you found this post helpful! It covered a little bit of everything involving building Flutter apps, from designing and planning to implementing, with a little technical details on the architecture as well as packages and approaches used.</p>
<p>I've been using Flow for myself for a few months and find it quite useful, so this is already a win for me! It's been a great experience building and publishing my second Flutter app. Since publishing my first Flutter app two years ago, Flutter's developer experience has improved, making it easier to develop with Flutter, and I even got to re-use some past learning from the first app. Perhaps my third app could be published even quicker!</p>
<p>Cover image generated by OpenAI's <a target="_blank" href="https://openai.com/dall-e-2">DALL-E</a>.</p>
<p>This article was published as the #week3 article for #4articles4weeks.</p>
]]></content:encoded></item><item><title><![CDATA[7 tips to take your Flutter skills to the next level]]></title><description><![CDATA[Introduction
As a Flutter developer, at some point in your learning path, you'll feel like you know enough about Flutter and are confident enough to build production-ready applications with Flutter.
Perhaps you've struggled a bit to pick the state ma...]]></description><link>https://dartling.dev/7-tips-to-take-your-flutter-skills-to-the-next-level</link><guid isPermaLink="true">https://dartling.dev/7-tips-to-take-your-flutter-skills-to-the-next-level</guid><category><![CDATA[Flutter]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[#week2]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Sat, 27 Aug 2022 09:37:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662202301925/ChWjyRDEn.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>As a Flutter developer, at some point in your learning path, you'll feel like you know enough about Flutter and are confident enough to build production-ready applications with Flutter.</p>
<p>Perhaps you've struggled a bit to pick the state management or routing approaches of your choice, but now you've even built some small or even big apps with Flutter. But is there more to learn about Flutter? Of course, there's always more to learn!</p>
<p>While you don't have to "force" yourself to level up your Flutter skills, since it's something that can happen the more you work with Flutter anyway, there are many ways to improve further as a Flutter developer. In this post, we will share some tips and resources on things you can do and learn to take your skills to the next level.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661592815127/cUqhZuV9p.png" alt="Blue bird in front of a whiteboard with equations" class="image--center mx-auto" /></p>
<h1 id="heading-flutter-deep-dives">Flutter deep dives</h1>
<p>It's easy to build with Flutter, thanks to its great user experience and learning resources out there. But have you been <a target="_blank" href="https://docs.flutter.dev/resources/inside-flutter">inside Flutter</a>, and do you know about its <a target="_blank" href="https://docs.flutter.dev/resources/architectural-overview">architecture</a>? You don't need to know about these, but it's nice to know a little more about the framework and how it works behind the scenes.</p>
<p>There are tons of resources out there going over the more complex aspects and features of Flutter, and learning about these can definitely help. They might seem elusive at first, but <a target="_blank" href="https://docs.flutter.dev/development/ui/animations">animations</a> and <a target="_blank" href="https://docs.flutter.dev/development/ui/advanced/slivers">slivers</a> are useful to know about, even if there are widgets and packages available that can simplify these by a lot for you.</p>
<p>Everyone likes to learn differently. Personally, I learn (and enjoy it) best when going over written tutorials such as blog posts, and building (example) apps on my own, but others might prefer videos and/or courses. Thankfully, Flutter's <a target="_blank" href="https://flutter.dev/learn">learning page</a> has plenty of resources of all kinds, and even separates them in Beginner, Intermediate and Advanced levels. And there are likely countless "unofficial" resources on any topic and in the format of your choice, just a simple search away!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661592860767/yC7rt8eFE.png" alt="Blue bird in a lab holding a test tube" class="image--center mx-auto" /></p>
<h1 id="heading-experiment">Experiment</h1>
<p>Depending on apps you've worked on, there might be things Flutter can do you've never heard of. For example, did you know you can <a target="_blank" href="https://flutter.dev/games">build games</a> with Flutter? Packages like <a target="_blank" href="https://pub.dev/packages/flame">Flame</a> make it very easy to get started as a Flutter game developer.</p>
<p><a target="_blank" href="https://pub.dev/">pub.dev</a> has tons of packages that can do interesting things which you can experiment with. You could build an app that uses <a target="_blank" href="https://pub.dev/packages?q=nfc">NFC</a>, the camera, bluetooth... There are many more advanced use cases for Flutter apps that you might never come across while working for a company or making your own apps.</p>
<p>Expanding your horizons (and skills!) as a Flutter developer is a great way to improve as a developer and learn more about what you can do with Flutter. Experimenting with things you haven't already worked with is one way to achieve this. You might even consider trying alternative approaches to what you've been used to so far, such as a different package for state management (or no package at all), routing and persistence/data storage. Or perhaps if you've only been building mobile apps, you could try out Flutter on desktop and web. It's a (slightly) different world there, as you need to adapt for bigger screens, potentially support multiple windows, and use different plugins (if they even exist!).</p>
<h1 id="heading-go-native">Go native</h1>
<p>It's (almost) inevitable that at some point while building apps with Flutter, you'll want to make use of some native platform features that are not available as plugins. For that, you have to know about <a target="_blank" href="https://docs.flutter.dev/development/platform-integration/platform-channels">platform channels</a>, but you'll also have to write native code for each platform you're supporting.</p>
<p>For the Flutter/Dart part, I've written some tutorials on <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code">building plugins platform channels</a> as well as doing the same but using the <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-with-pigeon">Pigeon package</a> which simplifies a few things compared to just using channels. The Flutter website also has useful resources on <a target="_blank" href="https://docs.flutter.dev/development/packages-and-plugins/developing-packages">developing plugins</a>.</p>
<p>It's good to at least know the basics of each native language of the platform you're targeting. If you're a mobile app dev, some Java/Kotlin knowledge is useful for Android, and Objective-C/Swift for iOS. For plugins or accessing some native features, it should be enough to just know the language, without needing to know how to build native UIs. However, things like home page widgets or watch (WearOS, Apple watch) apps are things you can't achieve with Flutter (yet?), so building a small native app for each platform might be good experience.</p>
<h1 id="heading-get-involved">Get involved</h1>
<p>There are many ways to get involved with the <a target="_blank" href="https://flutter.dev/community">Flutter community</a>, from online and offline events/meet-ups, to the Discord server and the Flutter subreddits. By being involved you can meet more people working with Flutter, as well as learn new things. You may even be able to help others who are earlier on their Flutter journey (for this, there is also <a target="_blank" href="https://stackoverflow.com/tags/flutter">StackOverflow</a>).</p>
<p>You could even contribute to open source Flutter packages or plugins, or even publish your own if you've built something that's not already available and want to share it with everyone else. You could even contribute to the Flutter SDK itself! There are currently over 5000 issues listed on the Flutter GitHub repo, and you could start by looking at ones marked with the <a target="_blank" href="https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+contribution%22">good first contribution</a> label.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661592893653/ca6i2e7AK.png" alt="Blue bird in a recording studio" class="image--center mx-auto" /></p>
<h1 id="heading-write-or-produce">Write or produce</h1>
<p>By writing in any form, either a personal blog or other sites, you can not only contribute to the community as well as teach and help others, but it can also be a great way to learn about things yourself. Of course, you might prefer to produce videos rather than write, and the same applies for that!</p>
<p>I've personally found writing tutorials very useful when I was interested to use a new technology with Flutter, such as <a target="_blank" href="https://dartling.dev/series/full-stack-flutter">Supabase</a>, or wanted to tackle some of the "harder" things in Flutter such as writing plugins.</p>
<h1 id="heading-stay-up-to-date">Stay up-to-date</h1>
<p>Besides Flutter news such as new versions and upgrades, which are usually easy to hear about, especially if you're on Twitter or Reddit, there are tons of resources, packages or apps using Flutter out there and being released every day. It's hard to keep up with everything, and can be very time-consuming.</p>
<p>Newsletters are a good way to keep up with the latest on Flutter. You can receive curated lists of articles, videos, news and packages weekly (or bi-weekly) in your inbox. There are other Flutter developers in the community that are happy to do the curation for you! Some popular newsletters include <a target="_blank" href="https://fluttertap.com/">Flutter Tap</a> and <a target="_blank" href="https://newsletter.neevash.dev/">The Flutter Bi-Weekly</a>. I'm also a fan of <a target="_blank" href="https://flutternewsletter.volpato.dev/">This week in Flutter</a> by <a target="_blank" href="https://twitter.com/_mvolpato">Michele</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661592925596/rUS4iA8ri.png" alt="Blue bird with its beak in front of a computer" class="image--center mx-auto" /></p>
<h1 id="heading-keep-building">Keep building</h1>
<p>Most importantly, though, you should just keep building! Having your own Flutter apps in production is already a big achievement, and simply by maintaining these apps and dealing with user feedback and requests, you would also be "maintaining" your Flutter skills, as wel as improving them, and staying up-to-date with any new changes.</p>
<h1 id="heading-wrapping-up">Wrapping up</h1>
<p>In this article, we shared some tips on improving your Flutter development skills and taking them to the next level. There are many ways to do this, and some might be obvious, but I hope you found some of these helpful! If there are more tips and/or resources you would like to share, please do so in the comments.</p>
<p>Images generated by OpenAI's <a target="_blank" href="https://openai.com/dall-e-2">DALL-E</a>.</p>
<p>This article was published as the #week2 article for #4articles4weeks.</p>
]]></content:encoded></item><item><title><![CDATA[Getting your first job as a Flutter developer]]></title><description><![CDATA[Introduction
It's 2022, and the tech job market is very hot right now. Not so much for Flutter jobs, unfortunately. Since Flutter is still relatively young, not too many companies currently use it, or even plan to do so in the future.
For mobile appl...]]></description><link>https://dartling.dev/getting-your-first-job-as-a-flutter-developer</link><guid isPermaLink="true">https://dartling.dev/getting-your-first-job-as-a-flutter-developer</guid><category><![CDATA[Flutter]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[week1]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Sun, 21 Aug 2022 14:57:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662202328197/tYY5raDxd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>It's 2022, and the tech job market is very hot right now. Not so much for Flutter jobs, unfortunately. Since Flutter is still relatively young, not too many companies currently use it, or even plan to do so in the future.</p>
<p>For mobile applications, some companies still prefer to go native, and the ones that do choose to go cross-platform seem to be more leaning towards React Native because it's been around longer and comes with the whole NPM ecosystem which is more mature than pub.dev.</p>
<p>For desktop applications, this is where Flutter would potentially be a great candidate as you can develop for Windows, Linux and macOS with one codebase. However, Flutter's support for these platforms is even younger than mobile, and depending on the complexity of the desktop applications there could still be a need for native code due to the lack of plugins. Similarly with mobile and React Native, companies might choose to use Electron, if not stick with native apps.</p>
<p>With all that in mind, you might still want to work with Flutter full-time anyway. Flutter's developer experience is pretty great after all, and so is the community. So, let's not lose hope! Even though there's currently way less demand for Flutter compared to other jobs in tech, it's still possible to land your first job with Flutter. After all, Flutter is a popular choice with new companies or startups, and more and more established companies are looking to adopt it.</p>
<p>In this post, we will go over some possible ways to find and apply for a Flutter job. We will cover some places to look for a job, as well as alternative places you could look at or things to do in order to find potential opportunities. In some cases, opportunities could find you!</p>
<h1 id="heading-go-through-job-boards">Go through job boards</h1>
<p>The most common way to find yourself a job to apply for is simply to start looking online. Any online search for "flutter developer job" could get you quite a few results. However, since there's not as many Flutter jobs out there, you could only come across either outdated job openings, or job descriptions that simply mention Flutter as a nice-to-have, with the job being mainly for a native developer position.</p>
<p><a target="_blank" href="https://www.linkedin.com/jobs/flutter-jobs">LinkedIn's job board</a> is a good place to look at to avoid coming across outdated listings. A lot of professionals are already on LinkedIn, and so are companies looking for developers. So if any companies out there are looking for Flutter developers, it's likely they've put this up on LinkedIn as well. Additionally, if you haven't listed Flutter as one of your skills on your LinkedIn profile, you should do so! Recruiters looking for Flutter developers could come across you when searching for candidates.</p>
<p>There are also a few Flutter-specific job boards out there, but a few are not really active or kept up-to-date. <a target="_blank" href="https://flutterjobs.info/jobs/all">Flutter Jobs</a> seems promising and they also have a <a target="_blank" href="https://twitter.com/flutter_jobs">Twitter account</a> and a mailing list which you could follow to be notified of any new job posts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661594006423/DcaCQurGx.png" alt="A blue bird wearing a cap in front of a computer" class="image--center mx-auto" /></p>
<h1 id="heading-start-with-an-internship">Start with an internship</h1>
<p>This was how I got my first job, but way before Flutter was stable! An internship is a great way to get started, especially if you've recently graduated from university or finished a programming course or bootcamp, and looking to start your career in software development.</p>
<p>As mentioned before, however, it might be even harder to find companies that are offering internships for Flutter developers. But if you do find one, there's a good chance the companies will be happy to help you get started with your Flutter career, and you could potentially move on to a full-time position with the company after your internship.</p>
<h1 id="heading-go-freelance-or-consulting">Go freelance or consulting</h1>
<p>Because a lot of new companies and startups could pick Flutter for their apps, it's possible to get a job as a freelancer or consult for companies that are just starting out and looking to use Flutter.</p>
<p>At places such as Upwork, you can sign up and either <a target="_blank" href="https://www.upwork.com/freelance-jobs/flutter">look for freelance jobs</a> or <a target="_blank" href="https://www.upwork.com/hire/flutter-freelancers/">get hired for an hourly rate</a>. There are also websites such as <a target="_blank" href="https://flexiple.com/">Flexiple</a> or Toptal where you can apply as a freelance developer and get assigned clients and projects.</p>
<p>If you have a good network and portfolio, you could even do that without needing a middleman, but you'd need to find your own clients and work which is more challenging. But if your network is good enough, instead it could be that clients find you!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661593894280/DqKQylrxN.png" alt="A blue bird and a world globe" class="image--center mx-auto" /></p>
<h1 id="heading-join-the-flutter-community">Join the Flutter community</h1>
<p>There are many benefits to being involved with the <a target="_blank" href="https://flutter.dev/community">Flutter community</a>. The Discord server in particular has a <code>#hiring</code> channel where companies or people looking for Flutter developers share information on their openings. There is also a <code>#for-hire</code> channel in which you can share your resume and portfolio, and anyone interested can reach out to you directly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661593790329/xfiX9p57u.png" alt="A blue bird writing" class="image--center mx-auto" /></p>
<h1 id="heading-write-about-flutter">Write about Flutter</h1>
<p>While this might not necessarily evolve into a full-time job, writing about Flutter is a great way to improve your Flutter skills as well as get your name out there as a competent Flutter developer.</p>
<h2 id="heading-start-a-blog">Start a blog</h2>
<p>The best way to learn something is by teaching it. By starting a blog, you could share some of the knowledge you pick up while working on your Flutter apps. Did you use some fancy new package in your app to do something cool? Write a blog post about it! Others could find this useful.</p>
<p>But you're looking for a job, and a salary, so a blog is unlikely to become something that will get you a proper developer salary (at least not very early!). However, simply having a blog out there is great for your resume, and I've personally had a couple of companies reach out to me on Dartling's email to offer me a job (or at least, to interview for one).</p>
<h2 id="heading-become-a-technical-writer-or-editor">Become a technical writer or editor</h2>
<p>You could also have a career as a technical writer, or simply do this on the side. Companies such as <a target="_blank" href="https://www.raywenderlich.com/11455688-write-or-edit-for-us">raywenderlich.com</a> and <a target="_blank" href="https://code.pieces.app/content-partner">Pieces</a> can pay you per article, and you can start with just a couple of articles per month. <a target="_blank" href="https://www.digitalocean.com/community/pages/write-for-digitalocean">Digital Ocean</a> will even donate to tech-focused charities in addition to compensating you!</p>
<p>A personal blog, or guest posts in other blogs could definitely help you land a technical writer job, so if you're interested, just get started! It's easy to get started on a tech blog with Hashnode, and it would be nice to see more Flutter-focused blogs on here also.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661594293266/laUwFnrCr.png" alt="A blue bird holding two phones" class="image--center mx-auto" /></p>
<h1 id="heading-publish-your-apps">Publish your apps</h1>
<p>If you're looking for a Flutter job, you've at least built a counter app with Flutter, and hopefully something a bit more complex. But publishing it to the Play Store, App Store or any store could potentially earn you some money if you implement in-app purchases. There are plenty of indie developers making a full-time income by building apps, either by going all-in one one app, or building several small ones. With the right idea and execution (Flutter helps with the execution!), you can earn money on your own, with your own apps.</p>
<p>Going indie and making your own Flutter apps as a full-time job is probably the hardest option of all mentioned here, however, at least in my opinion. There's no guarantee you will get to earn as much as you would by working full-time for a company as a developer, and it could take a while to get there. I released <a target="_blank" href="https://timelog.link/">my first Flutter mobile app</a> a couple of years ago and I'm perfectly happy with the few coffees and lunches I get from its in-app purchases every month. It's something I'd really recommend having on the side, and it's also a great addition to your portfolio!</p>
<h1 id="heading-wrapping-up">Wrapping up</h1>
<p>In this article, we listed a few ways you can go about finding a job as a Flutter developer. It can be challenging, but your best best is to keep looking and applying to any opportunities you come across and are interested in. Freelancing is a viable alternative but can be more challenging, and perhaps a bit less stable than a full-time job.</p>
<p>In the meantime, getting more involved with the Flutter community, perhaps writing about Flutter and publishing and iterating on your Flutter apps is a great way to improve both your Flutter skills and your portfolio, as well as expand your network. These could also get more job opportunities coming your way, as well as increase your chances of getting hired next time you apply for a job as Flutter developer.</p>
<p>If you're reading this and looking for your first job as a Flutter developer, we wish you good luck! And if you have managed to land a job in a way not mentioned here, do let us know in the comments.</p>
<p>Images generated by OpenAI's <a target="_blank" href="https://openai.com/dall-e-2">DALL-E</a>.</p>
<p>This article was published as the #week1 article for #4articles4weeks.</p>
]]></content:encoded></item><item><title><![CDATA[How to create a custom plugin in Flutter with Pigeon]]></title><description><![CDATA[Introduction
In this tutorial, we will create a Flutter plugin which targets the Android and iOS platforms using the Pigeon package.
With Flutter being a UI framework, communicating with the native platform is not something we always need or use. And...]]></description><link>https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-with-pigeon</link><guid isPermaLink="true">https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-with-pigeon</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[plugins]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Mon, 11 Jul 2022 18:03:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657481494421/YWXn3kk1z.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial, we will create a Flutter plugin which targets the Android and iOS platforms using the Pigeon package.</p>
<p>With Flutter being a UI framework, communicating with the native platform is not something we always need or use. And usually when we do need to do so, there are tons of packages out there that already do this for specific use cases.</p>
<p>But there are cases when the functionality the native platform has to offer is not yet available in a public package on <a target="_blank" href="https://pub.dev">pub.dev</a>. In these cases, it's up to us! We need to write some Dart code on the Flutter side to call the platform, as well as the native platform code to call the functionality we need (and that's once per platform your app supports!).</p>
<h2 id="heading-native-platform-communication">Native platform communication</h2>
<h3 id="heading-platform-channels">Platform channels</h3>
<p>One way to communicate with the native platform in Flutter is by using a <code>MethodChannel</code>. We covered how to build a plugin or call native platform code in Flutter using method channels in a <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code">previous post</a>.</p>
<p>While using <code>MethodChannel</code> is relatively straightforward, it can actually be quite time-consuming; when calling methods through the <code>MethodChannel</code>, you can only pass arguments of simple types such as <code>int</code>, <code>double</code>, <code>bool</code> or <code>String</code>, as well as maps of list with such types as values. You can see the full data types support <a target="_blank" href="https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-java-tab#codec">here</a>. If you need to pass a complex object as an argument, you would need to have logic to parse this to, let's say, a map of string keys to their values. In addition, you need to do the same for any data that is returned from the native platform.</p>
<p>We could work around having to introduce parsing logic by using a package such as <a target="_blank" href="https://pub.dev/packages/json_serializable"><code>json_serializable</code></a> to parse data to and from JSON to save ourselves some time. However, you'd need to make sure the native platforms are returning the data in the exact format you are expecting, and vice versa. Otherwise the parsing will fail.</p>
<h3 id="heading-pigeon">Pigeon</h3>
<p>Pigeon is a code generator package which generates all the code necessary to communicate between Flutter and any host platform. All you have to do is define the API. This is convenient, because you don't have to worry about any parsing logic, and the communication is guaranteed to be type-safe.</p>
<p>As of July 10th 2022, Pigeon only supports Android and iOS, and generates Java and Objective-C code (Swift is experimental) respectively. The generated code is still accessible to Kotlin or Swift. There is also experimental Windows support with C++.</p>
<h2 id="heading-creating-a-plugin">Creating a plugin</h2>
<p>In this post, we will create a simple plugin using Pigeon. What we will build will be identical to the (fake) app usage plugin we built <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code">previously</a>, so we can compare the results.</p>
<p>The plugin is simple; it should return a list of all apps and their usage, as well as support the ability to set time limits on specific apps. We won't actually implement this functionality on the native side as it's outside the scope of this tutorial, but rather return dummy data from the native side instead.</p>
<h3 id="heading-the-plugin-template">The plugin template</h3>
<p>To get started, we'll create a Flutter plugin using <code>flutter create</code> with the plugin template.</p>
<pre><code class="lang-sh">flutter create --org dev.dartling --template=plugin --platforms=android,ios app_usage_pigeon
</code></pre>
<p>This will generate the code for the plugin, as well as an example project that uses this plugin. By default, the generated Android code will be in Kotlin, and iOS in Swift, but you can specify either Java or Objective-C with the -a and -i flags respectively. (<code>-a java</code> and/or <code>-i objc</code>).</p>
<p>There is quite a bit of code included with the plugin template. We go into the Dart, Kotlin and Swift code into more detail in <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code">this post</a>, if you're curious. For the context of this tutorial, it's enough to know the following:</p>
<p>On the Dart side, there are three classes:</p>
<ul>
<li><code>AppUsagePlatform</code> - the "interface"/API of the plugin, which implements <code>PlatformInterface</code>.</li>
<li><code>MethodChannelAppUsage</code> - an implementation of <code>AppUsagePlatform</code> using method channels.</li>
<li><code>AppUsage</code> - the class exposing the methods to be used by any apps which need to use our plugin.</li>
</ul>
<p>The Kotlin code generated is <code>AppUsagePlugin.kt</code>, which uses method channels. It is defined in our <code>pubspec.yaml</code> as the plugin class for the Android platform, so we'll still be needing it, though we will make some changes to it later. The same applies to the Swift code, which includes <code>SwiftAppUsagePlugin.swift</code> as well as <code>AppUsagePlugin.h</code> and <code>AppUsagePlugin.m</code>.</p>
<p>In this tutorial, we will write an implementation of <code>AppUsagePlatform</code> which uses Pigeon rather than method channels. We can delete <code>MethodChannelAppUsage</code> as we won't be needing it.</p>
<p>Note: the new plugin template using <code>PlatformInterface</code> introduces quite a bit of code that you might not really need if you just want to call some native code in your app. If you wanted, you could still use Pigeon without creating a plugin or a separate package, but that's what we'll be doing in this tutorial.</p>
<h2 id="heading-using-pigeon">Using Pigeon</h2>
<h3 id="heading-installing-the-pigeon-package">Installing the <code>pigeon</code> package</h3>
<p>Let's install the package:</p>
<pre><code class="lang-sh">flutter pub add --dev pigeon
</code></pre>
<p>Alternatively, add this to your <code>pubspec.yaml</code>:</p>
<pre><code class="lang-sh">dev_dependencies:
  pigeon: ^3.2.3
</code></pre>
<h3 id="heading-defining-the-app-usage-api">Defining the App Usage API</h3>
<p>The way Pigeon works is pretty simple; we define our API in a Dart class outside the <code>lib</code> folder (as Pigeon is a dev dependency). The API class should be an abstract class with the <code>@HostApi()</code> decorator, and its methods should have the <code>@async</code> decorator.</p>
<p>Let's define our App Usage API in a new directory named <code>pigeons</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// pigeons/app_usage_api.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:pigeon/pigeon.dart'</span>;

<span class="hljs-keyword">enum</span> State { success, error }

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StateResult</span> </span>{
  <span class="hljs-keyword">final</span> State state;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> message;

  StateResult(<span class="hljs-keyword">this</span>.state, <span class="hljs-keyword">this</span>.message);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsedApp</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> minutesUsed;

  UsedApp(<span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">this</span>.name, <span class="hljs-keyword">this</span>.minutesUsed);
}

<span class="hljs-meta">@HostApi</span>()
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsageApi</span> </span>{
  <span class="hljs-meta">@async</span>
  <span class="hljs-built_in">String?</span> getPlatformVersion();

  <span class="hljs-meta">@async</span>
  <span class="hljs-built_in">List</span>&lt;UsedApp&gt; getApps();

  <span class="hljs-meta">@async</span>
  StateResult setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">int</span> minutesUsed);
}
</code></pre>
<h3 id="heading-caveats-and-limitations">Caveats and limitations</h3>
<p>Defining the API was relatively simple, but there are a few things to mention:</p>
<h4 id="heading-futures">Futures</h4>
<p>We do not need to specify the return values as <code>Future</code>s, but in the generated code they will be. So <code>getPlatformVersion</code> will actually return a <code>Future&lt;String?&gt;</code> in the generated Dart code.</p>
<h4 id="heading-no-imports-allowed">No imports allowed</h4>
<p>No imports other than <code>package:pigeon/pigeon.dart</code> are allowed. This means EVERY model class should be defined in this Pigeon API file.</p>
<h3 id="heading-supported-data-types">Supported data types</h3>
<p>As mentioned before, only simple JSON-like values are supported. This means we can't use useful Dart types such as <code>DateTime</code> or <code>Duration</code>. Which means we might still need additional mapping logic to convert the Pigeon model to the model we want to use within the app. For <code>minutesUsed</code> in <code>UsedApp</code>, we'll need to manually create a <code>Duration</code> out of the minutes, though it would be nice to have this as a <code>Duration</code> in the first place.</p>
<h4 id="heading-enums-arent-yet-supported-for-primitive-return-types">Enums aren't yet supported for primitive return types</h4>
<p>We cannot return an enum from a method, but we can have an enum as a method parameter. We can still return enums, but only if we wrap them in a separate class, like below.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Not valid, enums cannot be returned.</span>
<span class="hljs-keyword">enum</span> ResultState { success, error }

<span class="hljs-meta">@HostApi</span>()
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsageApi</span> </span>{
  <span class="hljs-meta">@async</span>
  ResultState getState();
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// Valid, enums can be method parameters and fields of returned objects.</span>
<span class="hljs-keyword">enum</span> ResultState { success, error }

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiResult</span> </span>{
  <span class="hljs-keyword">final</span> ResultState state;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> message;

  ApiResult(<span class="hljs-keyword">this</span>.state, <span class="hljs-keyword">this</span>.message);
}

<span class="hljs-meta">@HostApi</span>()
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsageApi</span> </span>{
  <span class="hljs-meta">@async</span>
  ApiResult getResult();

  <span class="hljs-meta">@async</span>
  <span class="hljs-keyword">void</span> setState(ResultState state);
}
</code></pre>
<h4 id="heading-generics-are-supported-but-can-only-be-used-with-nullable-types">Generics are supported, but can only be used with nullable types</h4>
<p>We can still define them as non-nullable in our <code>HostApi</code> definition, e.g. <code>List&lt;Something&gt;</code>, but the generated Dart class will have <code>List&lt;Something?&gt;</code> instead.</p>
<h3 id="heading-generating-the-code">Generating the code</h3>
<h4 id="heading-running-the-generator">Running the generator</h4>
<p>After defining the API, we can generate code using <code>flutter pub run pigeon</code>. This command requires quite a few arguments:</p>
<pre><code class="lang-sh">flutter pub run pigeon \
  --input pigeons/app_usage_api.dart \
  --dart_out lib/app_usage_api.dart \
  --java_package <span class="hljs-string">"dev.dartling.app_usage"</span> \
  --java_out android/src/main/java/dev/dartling/app_usage/AppUsage.java \
  --experimental_swift_out ios/Classes/AppUsage.swift
</code></pre>
<p>We will store this in a <code>pigeon.sh</code> file, just so it's easy to find and run in the future.</p>
<p>We're going with Swift which has experimental support for now rather than Objective-C, but for Objective-C we can simply drop the <code>experimental_swift_out</code> argument in favor of these three:</p>
<pre><code class="lang-sh">  --objc_header_out ios/Classes/AppUsageApi.h \
  --objc_source_out ios/Classes/AppUsageApi.m \
  --objc_prefix FLT
</code></pre>
<h5 id="heading-dart">Dart</h5>
<p>The <code>input</code> argument should be the file we defined the API in, and <code>dart_out</code> should be in our <code>lib</code> folder, as it's the code we'll actually be using in our app.</p>
<h5 id="heading-java">Java</h5>
<p><code>java_package</code> is the full package name, in this case <code>dev.dartling.app_usage</code> and <code>java_out</code> is the path to the Java file that will be generated.</p>
<p>Note: Make sure the generated Java class name does NOT match the name of the Pigeon <code>HostApi</code>. In our case, the generated Java class will be <code>AppUsage</code>, and will include a nested public <code>AppUsageApi</code> interface, taken from the <code>HostApi</code> class name defined in Dart. If we used the same names (which is what I did initially!), compilation will fail due to duplicate names.</p>
<p>Note: if your plugin template uses Kotlin, like we did in this one, you will need to create the <code>java/dev/dartling/app_usage</code> directory manually under <code>src/main</code>, as only <code>kotlin/dev/dartling/app_usage</code> was generated as part of the plugin template.</p>
<h5 id="heading-swift">Swift</h5>
<p><code>experimental_swift_out</code> is the path to the Swift file that will be generated.</p>
<h5 id="heading-objective-c">Objective-C</h5>
<p>The <code>objc_header_out</code> and <code>objc_source_out</code> arguments determine the generated files on the Objective-C side, and the <code>objc_prefix</code> is optional and determines the prefix of the generated class names.</p>
<h4 id="heading-understanding-the-generated-code">Understanding the generated code</h4>
<p>The code generated by Pigeon after running <code>flutter pub run pigeon</code> is not something we should really have to look at often. All we need to know is that the Java class will have an <code>AppUsageApi</code> interface which our implementation class should implement; this can be both in either Java or Kotlin. In Objective-C, there will be a <code>FLTAppUsageApi</code> protocol equivalent (notice the <code>FLT</code> prefix which is an argument when running the generator), and in Swift an <code>AppUsageApi</code> protocol.</p>
<h3 id="heading-native-platform-implementation">Native platform implementation</h3>
<h4 id="heading-android">Android</h4>
<p>We have our interface, now all we need to do is have an implementation for it. To keep things simple, we will simply the existing <code>AppUsagePlugin</code> Kotlin class to implement <code>AppUsageApi</code>, in addition to the existing <code>FlutterPlugin</code> interface.</p>
<p>Here is the full class:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsagePlugin.kt</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlugin</span> : <span class="hljs-type">FlutterPlugin</span>, <span class="hljs-type">AppUsageApi {</span></span>
    <span class="hljs-keyword">val</span> usedApps: MutableList&lt;UsedApp&gt; = mutableListOf(
        usedApp(<span class="hljs-string">"com.reddit.app"</span>, <span class="hljs-string">"Reddit"</span>, <span class="hljs-number">75</span>),
        usedApp(<span class="hljs-string">"dev.hashnode.app"</span>, <span class="hljs-string">"Hashnode"</span>, <span class="hljs-number">37</span>),
        usedApp(<span class="hljs-string">"link.timelog.app"</span>, <span class="hljs-string">"Timelog"</span>, <span class="hljs-number">25</span>),
    )

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onAttachedToEngine</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> flutterPluginBinding: <span class="hljs-type">FlutterPlugin</span>.<span class="hljs-type">FlutterPluginBinding</span>)</span></span> {
        AppUsageApi.setup(flutterPluginBinding.binaryMessenger, <span class="hljs-keyword">this</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getPlatformVersion</span><span class="hljs-params">(result: <span class="hljs-type">Result</span>&lt;<span class="hljs-type">String</span>&gt;?)</span></span> {
        result?.success(<span class="hljs-string">"Android <span class="hljs-subst">${android.os.Build.VERSION.RELEASE}</span>"</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getApps</span><span class="hljs-params">(result: <span class="hljs-type">Result</span>&lt;<span class="hljs-type">MutableList</span>&lt;<span class="hljs-type">UsedApp</span>&gt;&gt;?)</span></span> {
        result?.success(usedApps);
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setAppTimeLimit</span><span class="hljs-params">(
        appId: <span class="hljs-type">String</span>,
        durationInMinutes: <span class="hljs-type">Long</span>,
        result: <span class="hljs-type">Result</span>&lt;<span class="hljs-type">TimeLimitResult</span>&gt;?
    )</span></span> {
        <span class="hljs-keyword">val</span> stateResult = TimeLimitResult.Builder()
            .setState(ResultState.success)
            .setMessage(<span class="hljs-string">"Timer of <span class="hljs-variable">$durationInMinutes</span> minutes set for app ID <span class="hljs-variable">$appId</span>"</span>)
            .build()
        result?.success(stateResult)
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">usedApp</span><span class="hljs-params">(id: <span class="hljs-type">String</span>, name: <span class="hljs-type">String</span>, minutesUsed: <span class="hljs-type">Long</span>)</span></span>: UsedApp {
        <span class="hljs-keyword">return</span> UsedApp.Builder()
            .setId(id)
            .setName(name)
            .setMinutesUsed(minutesUsed)
            .build();
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onDetachedFromEngine</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> binding: <span class="hljs-type">FlutterPlugin</span>.<span class="hljs-type">FlutterPluginBinding</span>)</span></span> {
        AppUsageApi.setup(binding.binaryMessenger, <span class="hljs-literal">null</span>)
    }
}
</code></pre>
<p>The <code>onAttachedToEngine</code> and <code>onDetachedFromEngine</code> are from <code>FlutterPlugin</code>. Previously they set things up to work for the method channel implementation. Now, we are calling the <code>AppUsageApi#setup</code> method to get it to work with Pigeon's generated code.</p>
<p>The other three functions we override are from the <code>AppUsageApi</code> interface. These are actually <code>void</code> functions, and we "return" the results by making use of <code>result</code>, which was actually generated as nullable. To return, we simply use <code>result?.success(...)</code>, and in case we want to throw an error, we can use <code>result?.error(...)</code> and pass a <code>Throwable</code>; this will be wrapped into a <code>PlatformException</code> on the Dart side.</p>
<h4 id="heading-ios">iOS</h4>
<p>Very similarly to Android, we will make the existing <code>SwiftAppUsagePlugin</code> implement the <code>AppUsageApi</code> protocol in addition to being a <code>FlutterPlugin</code>. Rather than <code>result</code>, we have <code>completion</code> which we call by passing the result as the argument. We also make some changes to <code>register</code> to use the static <code>AppUsageApiSetup#setUp</code> function to set things up with the generated file.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SwiftAppUsagePlugin</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">FlutterPlugin</span>, <span class="hljs-title">AppUsageApi</span> </span>{
    <span class="hljs-keyword">var</span> usedApps = [
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"com.reddit.app"</span>, name: <span class="hljs-string">"Reddit"</span>, minutesUsed: <span class="hljs-number">75</span>),
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"dev.hashnode.app"</span>, name: <span class="hljs-string">"Hashnode"</span>, minutesUsed:<span class="hljs-number">37</span>),
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"link.timelog.app"</span>, name: <span class="hljs-string">"Timelog"</span>, minutesUsed: <span class="hljs-number">25</span>)
    ]

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">register</span><span class="hljs-params">(with registrar: FlutterPluginRegistrar)</span></span> {
        <span class="hljs-keyword">let</span> messenger : <span class="hljs-type">FlutterBinaryMessenger</span> = registrar.messenger()
        <span class="hljs-keyword">let</span> api : <span class="hljs-type">AppUsageApi</span> &amp; <span class="hljs-type">NSObjectProtocol</span> = <span class="hljs-type">SwiftAppUsagePlugin</span>.<span class="hljs-keyword">init</span>()
        <span class="hljs-type">AppUsageApiSetup</span>.setUp(binaryMessenger: messenger, api: api)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getPlatformVersion</span><span class="hljs-params">(completion: @escaping <span class="hljs-params">(String?)</span></span></span> -&gt; <span class="hljs-type">Void</span>) {
        completion(<span class="hljs-string">"iOS "</span> + <span class="hljs-type">UIDevice</span>.current.systemVersion)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getApps</span><span class="hljs-params">(completion: @escaping <span class="hljs-params">([UsedApp])</span></span></span> -&gt; <span class="hljs-type">Void</span>) {
        completion(usedApps)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setAppTimeLimit</span><span class="hljs-params">(appId: String, durationInMinutes: Int32, completion: @escaping <span class="hljs-params">(TimeLimitResult)</span></span></span> -&gt; <span class="hljs-type">Void</span>) {
        completion(<span class="hljs-type">TimeLimitResult</span>(state: <span class="hljs-type">ResultState</span>.success, message: <span class="hljs-string">"Timer of \(durationInMinutes) minutes set for app ID \(appId)"</span>))
    }
}
</code></pre>
<p>Note: I've faced some weird issues with the iOS build sometimes not succeeding due to <code>AppUsageApi</code> not being found in the scope. If you run into the same issue, the quick hacky way is to copy everything in the generated <code>AppUsage.swift</code> into the existing <code>SwiftAppUsagePlugin.swift</code> file. Then it should work! If you figure out how/why this happens and how to fix it, please let me know in the comments!</p>
<h2 id="heading-using-the-plugin">Using the plugin</h2>
<p>We now have everything in place. The native platform implementations are done, and the <code>AppUsageApi</code> Dart class can be used to communicate with the native platforms.</p>
<p>All we have to do is create an instance of <code>AppUsageApi</code> and invoke its method... but wait, we're building a plugin! We should not use <code>AppUsageApi</code> directly (though we could!). Remember the <code>MethodChannelAppUsage</code> Dart class we deleted a while ago? We need to introduce an alternative that will use Pigeon instead of method channels.</p>
<p>Firstly, let's add make sure all methods that are part of our Pigeon <code>HostApi</code> are also defined in our <code>AppUsagePlatform</code>.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlatform</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">PlatformInterface</span> </span>{
  ...

  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() {
    <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'platformVersion() has not been implemented.'</span>);
  }

  Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'apps has not been implemented.'</span>);
  }

  Future&lt;TimeLimitResult&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'setAppTimeLimit() has not been implemented.'</span>);
  }
}
</code></pre>
<p>Now, our <code>AppUsagePlatform</code> implementation with Pigeon is very simple. We're simply going to invoke the methods of <code>AppUsageApi</code>, which was generated by Pigeon.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_pigeon.dart</span>
<span class="hljs-comment">/// <span class="markdown">An implementation of [AppUsagePlatform] that uses Pigeon.</span></span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PigeonAppUsage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppUsagePlatform</span> </span>{
  <span class="hljs-keyword">final</span> AppUsageApi _api = AppUsageApi();

  <span class="hljs-meta">@override</span>
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() {
    <span class="hljs-keyword">return</span> _api.getPlatformVersion();
  }

  <span class="hljs-meta">@override</span>
  Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps {
    <span class="hljs-keyword">return</span> _api
        .getApps()
        .then((apps) =&gt; apps.where((e) =&gt; e != <span class="hljs-keyword">null</span>).map((e) =&gt; e!).toList());
  }

  <span class="hljs-meta">@override</span>
  Future&lt;TimeLimitResult&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">return</span> _api.setAppTimeLimit(appId, duration.inMinutes);
  }
}
</code></pre>
<p>Note that in <code>apps</code> we filter null values and use the <code>!</code> operator, as <code>AppUsageApi#getApps()</code> returns <code>List&lt;UsedApp?&gt;</code>, due to Pigeon's current limitations.</p>
<p>Lastly, <code>AppUsage</code>, our main plugin class, should also be updated. All it does is delegate method calls to <code>AppUsagePlatform.instance</code>.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsage</span> </span>{
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() {
    <span class="hljs-keyword">return</span> AppUsagePlatform.instance.getPlatformVersion();
  }

  Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps {
    <span class="hljs-keyword">return</span> AppUsagePlatform.instance.apps;
  }

  Future&lt;TimeLimitResult&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) {
    <span class="hljs-keyword">return</span> AppUsagePlatform.instance.setAppTimeLimit(appId, duration);
  }
}
</code></pre>
<p>And let's not forget, that <code>AppUsagePlatform.instance</code> should now return an instance of <code>PigeonAppUsage</code> rather than <code>MethodChannelAppUsage</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlatform</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">PlatformInterface</span> </span>{
  ...

  <span class="hljs-keyword">static</span> AppUsagePlatform _instance = PigeonAppUsage();

  <span class="hljs-comment">/// <span class="markdown">The default instance of [AppUsagePlatform] to use.</span></span>
  <span class="hljs-comment">///
  <span class="markdown">/// Defaults to [PigeonAppUsage].</span></span>
  <span class="hljs-keyword">static</span> AppUsagePlatform <span class="hljs-keyword">get</span> instance =&gt; _instance;

  ...
}
</code></pre>
<p>I won't share snippets of the UI code and widgets, but you can take look at these <a target="_blank" href="https://github.com/dartling/app_usage_pigeon/blob/main/example/lib/main.dart">here</a>. Using the plugin in any app is simple; we initialize an instance of <code>AppUsage</code> and call its methods we need them.</p>
<p>Here is the final result:</p>
<table>
<tr>
<td><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648588947386/x81wxSmPZ.png" alt="android.png" /></td>
<td><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648588469952/DCIGFo5NI.png" alt="ios.png" /></td>
</tr>
</table>

<h2 id="heading-comparing-pigeon-and-method-channels">Comparing Pigeon and method channels</h2>
<p>We built an almost identical plugin and example app in a <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code">previous article</a>, so we can compare Pigeon with using method channels.</p>
<p>Overall, Pigeon is definitely an improvement. We only have to define our API and models once; the generated Android/iOS will include these models for us.</p>
<p>We also don't have to worry about serializing data we want to pass to the platform side or deserialize data coming from the platform side, and the opposite for the platform side; we won't have to worry about deserializing data coming from the Dart side and serializing data we return to the Dart side.</p>
<p>Thanks to the two points above, we needed significantly less lines of code to write a plugin using Pigeon rather than method channels.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we introduced Pigeon as a way to simplify native platform communication, and created a custom Flutter plugin with Android and iOS implementations to call (fake) native functionality, using Pigeon rather than method channels.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/app_usage_pigeon">here</a>.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic theme color with Material 3 (You) in Flutter]]></title><description><![CDATA[Introduction
In this blog post, we will enhance our app's theme to use dynamic colors taken from Material 3's OS-defined color scheme.
In Android 12, Material You, the third iteration of Material Design was introduced. One of the main features of Mat...]]></description><link>https://dartling.dev/dynamic-theme-color-material-3-you-flutter</link><guid isPermaLink="true">https://dartling.dev/dynamic-theme-color-material-3-you-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Material Design]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Mon, 04 Jul 2022 19:59:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1656961788271/dXg6nyawm.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, we will enhance our app's theme to use dynamic colors taken from Material 3's OS-defined color scheme.</p>
<p>In Android 12, Material You, the third iteration of Material Design was introduced. One of the main features of Material 3 is <em>Dynamic Color</em>, which allows users to select their own color scheme for the whole OS, derived from the wallpaper.</p>
<p>This results in a set of primary, secondary and tertiary colors being consistent across the whole OS as well as built-in apps such as the clock, calculator, and even some Google apps such as Photos.</p>
<p>You may not always want implement this for your apps, especially if your app needs to follow specific brand guidelines (and if color is an important part of your brand). However, in some cases, supporting a dynamic color theme might make sense, depending on your app and brand.</p>
<p>In this post, we will enhance the default Flutter counter app with dynamic color, using the <code>dynamic_color</code> Flutter package provided by the Material team.</p>
<h2 id="heading-using-material-3">Using Material 3</h2>
<p>For this tutorial, we will be working with the basic Flutter counter app example, but with one small change. We will change the provided theme from the below:</p>
<pre><code class="lang-dart">ThemeData(
    primarySwatch: Colors.blue,
)
</code></pre>
<p>To this:</p>
<pre><code class="lang-dart">ThemeData(
    colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
    useMaterial3: <span class="hljs-keyword">true</span>,
)
</code></pre>
<p>We do this in order to enable Material 3 using the <code>useMaterial3</code> flag. Not all widgets in Flutter are "Material 3-ready" as of yet, so we need to enable this explicitly.</p>
<p>We replace <code>primarySwatch</code> with <code>colorScheme</code>, but these actually do the same thing. However, <code>colorScheme</code> is now actually the preferred way to configure colors. We also do this because we will use <code>colorScheme</code> for the dynamic color.</p>
<p>We've also added three boxes showing the color scheme's primary, secondary and tertiary colors. This is so we can compare and see what happens to the color scheme once we've implemented dynamic color.</p>
<p>Here is how our app looks before and after this, if you're curious.</p>
<table>
<td>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656964140983/i0BzetQoL.png" alt="before_material3.png" />
</td>
<td>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656964161972/YlQ19pzFj.png" alt="after_material3.png" />
</td>
</table>

<h2 id="heading-using-dynamic-color">Using dynamic color</h2>
<h3 id="heading-the-dynamiccolor-package">The <code>dynamic_color</code> package</h3>
<p>The Material team has already created a package to help with this. It returns a Material color scheme based on a platform's implementation of dynamic color. This is actually not specific to just Material 3 and Android!</p>
<p>From the package's <a target="_blank" href="https://github.com/material-foundation/material-dynamic-color-flutter">GitHub repo page</a>, here is what you get from each platform:</p>
<ul>
<li>Android (12 and up) - color from the wallpaper</li>
<li>Linux - GTK+ theme's <code>@theme_selected_bg_color</code></li>
<li>macOS - app accent color</li>
<li>Windows - accent color or window/glass color</li>
</ul>
<p>Let's install the package:</p>
<pre><code class="lang-sh">flutter pub add dynamic_color
</code></pre>
<p>Alternatively, add this to your <code>pubspec.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">dynamic_color:</span> <span class="hljs-string">^1.4.0</span>
</code></pre>
<h3 id="heading-the-dynamiccolorbuilder-widget">The <code>DynamicColorBuilder</code> widget</h3>
<p>Let's now make use of the <code>dynamic_color</code> package to actually use dynamic colors. We can do this with the <code>DynamicColorBuilder</code>, a builder widget that uses a plugin under the hood to fetch the dynamic color from the OS and returns a light and dark color scheme.</p>
<pre><code class="lang-dart">DynamicColorBuilder({
    Key? key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.builder,
})
</code></pre>
<p><code>DynamicColorBuilder</code> accepts an optional key and requires a builder which should return whatever widget we want to enhance with dynamic colors. The builder widget's signature looks like this:</p>
<pre><code class="lang-dart">Widget <span class="hljs-built_in">Function</span>(
    ColorScheme? lightDynamic,
    ColorScheme? darkDynamic,
)
</code></pre>
<p>We can return a widget, any widget, and the builder includes a light dynamic color scheme as well as a dark dynamic color scheme. Both color schemes are nullable; if the OS does not respond or the platform does not support dynamic color (like with older Android versions), it returns <code>null</code>.</p>
<h3 id="heading-using-dynamic-color-schemes">Using dynamic color schemes</h3>
<p>We now have the <code>dynamic_color</code> package installed, and know how the <code>DynamicColorBuilder</code> widget works. Let's use it to make use of the system's color scheme.</p>
<p>Here's what the <code>build</code> method of our root <code>MyApp</code> widget looks like now:</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Flutter Demo'</span>,
      theme: ThemeData(
        colorScheme: lightColorScheme ?? _defaultLightColorScheme,
        useMaterial3: <span class="hljs-keyword">true</span>,
      ),
      darkTheme: ThemeData(
        colorScheme: darkColorScheme ?? _defaultDarkColorScheme,
        useMaterial3: <span class="hljs-keyword">true</span>,
      ),
      themeMode: ThemeMode.dark,
      home: <span class="hljs-keyword">const</span> MyHomePage(title: <span class="hljs-string">'Flutter Demo Home Page'</span>),
    );
  });
}
</code></pre>
<p>We've wrapped the <code>MaterialApp</code> widget with <code>DynamicColorBuilder</code>. For the theme, we replaced the previous <code>colorScheme</code> with the light color scheme provided by the builder. We also provided a <code>darkTheme</code> in addition to <code>theme</code>, which uses the dark color scheme provided by the builder. Note that since both color schemes are nullable, we have default color schemes, which we've extracted to static constants.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> _defaultLightColorScheme =
    ColorScheme.fromSwatch(primarySwatch: Colors.blue);

<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> _defaultDarkColorScheme = ColorScheme.fromSwatch(
    primarySwatch: Colors.blue, brightness: Brightness.dark);
</code></pre>
<p>These are exactly what we had before, with additionally the dark color scheme which we define in exactly the same way, plus the <code>dark</code> brightness value.</p>
<p>And that's it! With just a trivial amount of lines of code added (less than 10 if you don't count the default color schemes), we have a theme with a color scheme which dynamically changes depending on your operating system's settings. Here's how it looks like now:</p>
<table>
<td>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656964209481/JBtzjdkLr.png" alt="dynamic_color_light.png" />
</td>
<td>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656964215822/lTYCTrYc_.png" alt="dynamic_color_dark.png" />
</td>
</table>

<p>A small note if you're implementing this for your app. In debug mode, when the app loads for the first time you may quickly see the app load with the default color scheme for a split second before refreshing with the dynamic color scheme. Not to worry though, this won't be the case for your production app!</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we showed how to implement dynamic color in themes in our Flutter app, to make use of the Material 3, or You, dynamic color feature available in Android 12.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/dynamic_color">here</a>.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Swipe actions in Flutter with the Dismissible widget]]></title><description><![CDATA[Introduction
Swipe actions, or swipe gestures, are something that's very common in mobile apps. In its most common form, the "swipe to dismiss" pattern is something you might have seen in email apps, for example, where you swipe left to delete an ema...]]></description><link>https://dartling.dev/swipe-actions-flutter-dismissible-widget</link><guid isPermaLink="true">https://dartling.dev/swipe-actions-flutter-dismissible-widget</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[swipe]]></category><category><![CDATA[Flutter SDK]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 14 Jun 2022 13:49:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655211924466/owMYWB6h8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Swipe actions, or swipe gestures, are something that's very common in mobile apps. In its most common form, the "swipe to dismiss" pattern is something you might have seen in email apps, for example, where you swipe left to delete an email. Some apps now even let you customize the action happening when you swipe left or right (hence swipe actions), and you don't necessarily have to dismiss something either; an example would be the "swipe to reply" pattern implemented in some messaging apps.</p>
<p>In this post, we will go over how to implement swipe actions in Flutter using the <code>Dismissible</code> widget. We will implement a "swipe to delete" action for a (very) simple messaging app, with a confirmation dialog, as well as a "swipe to star (favorite)" action.</p>
<p>In case you're still unsure about what a "swipe action" looks, here's what we'll be building in this post:</p>
<center><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655212872632/Htwqmhw7u.gif" alt="swipe_actions.gif" /></center>

<p>At the end, if actions for swiping left or right are not enough for you, we will show how to easily implement multiple "slide" actions with the <code>flutter_slidable</code> package.</p>
<h2 id="heading-the-dismissible-widget">The <code>Dismissible</code> widget</h2>
<p>We can make any widget "dismissible" by wrapping it with the <code>Dismissible</code> widget:</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  child: <span class="hljs-keyword">const</span> ListTile(
    leading: Icon(Icons.flutter_dash),
    title: Text(<span class="hljs-string">'Dash'</span>),
    subtitle: Text(<span class="hljs-string">'Hello!'</span>),
  ),
),
</code></pre>
<p>At a minimum, this widget requires a child, which can be any widget we want to dismiss/swipe/slide, as well as a key. With the above code snippet, what we have is a regular <code>ListTile</code> widget which disappears when swiped in any horizontal direction.</p>
<h3 id="heading-directions">Directions</h3>
<p>By default, a dismissible widget can be swiped left or right. But we can customize this.</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  direction: DismissDirection.endToStart,
  onDismissed: (DismissDirection direction) {
    log(<span class="hljs-string">'Dismissed with direction <span class="hljs-subst">$direction</span>'</span>);
  },
  child: ...
),
</code></pre>
<p>With <code>endToStart</code>, our widget can only be swiped from its end to the start. In our case, this is a swipe to the left. However, this is either right-to-left or left-to-right depending on the reading direction of the Flutter app's locale. English is read from left to right, so <code>endToStart</code> is right to left.</p>
<p>Below are the possible <code>DismissDirection</code> values:</p>
<ul>
<li><code>endToStart</code> - Can be swiped left (or right depending on reading direction).</li>
<li><code>startToEnd</code> - Can be swiped right (or left depending on reading direction).</li>
<li><code>horizontal</code> - Can be swiped both left and right.</li>
<li><code>up</code> - Can be swiped up.</li>
<li><code>down</code> - Can be swiped down.</li>
<li><code>vertical</code> - Can be swiped both up and down.</li>
<li><code>none</code> - Cannot be swiped, or dismissed.</li>
</ul>
<p>The <code>none</code> value might seem a bit redundant, as you could just not wrap a widget with the <code>Dismissible</code> widget at all. But there might be cases where you have a list of dismissible widgets, and might want to dynamically choose whether a specific item in the list can be dismissed or not. In that case, you could use <code>none</code>.</p>
<p>The <code>onDismissed</code> callback is called after a widget is swiped and dismissed. It is called with the direction it was dismissed in. If your widget should only be swiped in one direction, you might not use this, but if you use the <code>horizontal</code> or <code>vertical</code> directions, you could check in which direction the widget was dismissed if you want to react differently for each direction.</p>
<h3 id="heading-swipe-but-dont-dismiss">Swipe, but don't dismiss</h3>
<p>The "swipe to dismiss" part has been quite easy so far, it already works! But what if you don't want to dismiss? The <code>confirmDismiss</code> callback parameter is called before the widget is dismissed, and returns a <code>Future&lt;bool&gt;</code>. If <code>false</code> is returned, the widget is not dismissed.</p>
<p>This helps if, for example, you want to a confirm a dismissal or deletion; you could show a dialog so that the user can confirm their action. Or maybe your swipe action might never dismiss the widget at all; in that case you'd just always return <code>false</code>.</p>
<h2 id="heading-implementing-swipe-actions">Implementing swipe actions</h2>
<p>Now that we have a basic <code>Dismissible</code> widget working, let's implement some swipe actions for our messaging app. When swiping left, we want to delete a message. When swiping right, we want to "star"/favorite the message.</p>
<h3 id="heading-swipe-to-delete">Swipe to delete</h3>
<p>Here's our widget so far:</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  direction: DismissDirection.endToStart,
  onDismissed: (DismissDirection direction) {
    log(<span class="hljs-string">'Dismissed with direction <span class="hljs-subst">$direction</span>'</span>);
    <span class="hljs-comment">// Your deletion logic goes here.</span>
  },
  child: <span class="hljs-keyword">const</span> ListTile(
    leading: Icon(Icons.flutter_dash),
    title: Text(<span class="hljs-string">'Dash'</span>),
    subtitle: Text(<span class="hljs-string">'Hello!'</span>),
  ),
),
</code></pre>
<p>This is really all we need for the "swipe to delete" action. The widget is dismissed as soon as we finish swiping, so for a basic demo this is enough. In a real app, you might have to add some additional logic in the <code>onDismissed</code> callback, for example to actually delete the message from your database.</p>
<h3 id="heading-confirm-dismissaldeletion">Confirm dismissal/deletion</h3>
<p>A problem with "destructive" swipe actions such as "swipe to delete" is that a user could easily swipe something left accidentally. For our deletion swipe action, we can make use of the <code>confirmDismiss</code> callback to ask the user if they really want to delete their message.</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  direction: DismissDirection.endToStart,
  onDismissed: (DismissDirection direction) {
    log(<span class="hljs-string">'Dismissed with direction <span class="hljs-subst">$direction</span>'</span>);
    <span class="hljs-comment">// Your deletion logic goes here.</span>
  },
  confirmDismiss: (DismissDirection direction) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> confirmed = <span class="hljs-keyword">await</span> showDialog&lt;<span class="hljs-built_in">bool</span>&gt;(
      context: context,
      builder: (context) {
        <span class="hljs-keyword">return</span> AlertDialog(
          title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Are you sure you want to delete?'</span>),
          actions: [
            TextButton(
              onPressed: () =&gt; Navigator.pop(context, <span class="hljs-keyword">false</span>),
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'No'</span>),
            ),
            TextButton(
              onPressed: () =&gt; Navigator.pop(context, <span class="hljs-keyword">true</span>),
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Yes'</span>),
            )
          ],
        );
      },
    );
    log(<span class="hljs-string">'Deletion confirmed: <span class="hljs-subst">$confirmed</span>'</span>);
    <span class="hljs-keyword">return</span> confirmed;
  },
  child: <span class="hljs-keyword">const</span> ListTile(
    leading: Icon(Icons.flutter_dash),
    title: Text(<span class="hljs-string">'Dash'</span>),
    subtitle: Text(<span class="hljs-string">'Hello!'</span>),
  ),
),
</code></pre>
<p>A bit lengthier than before, but if we extract the <code>AlertDialog</code> part to a method or widget, the code is not very complicated.</p>
<h3 id="heading-background">Background</h3>
<p>Right now, it's not clear that swiping will actually delete the message, even if we do show a confirmation dialog. We can specify a <code>background</code> parameter, which is of course a widget, and show a colored background with an icon.</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  direction: DismissDirection.endToStart,
  ...
  background: <span class="hljs-keyword">const</span> ColoredBox(
    color: Colors.red,
    child: Align(
      alignment: Alignment.centerRight,
      child: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Icon(Icons.delete, color: Colors.white),
      ),
    ),
  ),
  child: <span class="hljs-keyword">const</span> ListTile(
    leading: Icon(Icons.flutter_dash),
    title: Text(<span class="hljs-string">'Dash'</span>),
    subtitle: Text(<span class="hljs-string">'Hello!'</span>),
  ),
),
</code></pre>
<p>Here's how the deletion flow looks below, with the confirmation dialog.</p>
<center><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655212907957/pz1z4tzB_.gif" alt="swipe_to_delete.gif" /></center>

<h3 id="heading-swipe-to-star">Swipe to star</h3>
<p>Now, here's where it gets a little trickier! For the "swipe to star" action, we don't want to dismiss the widget after swiping to the right, but we also want to behave differently depending on the swipe direction.</p>
<p>First, we change the direction of our <code>Dismissible</code> widget to <code>horizontal</code>, as we want to swipe both left as well as right. Second, both our <code>onDismissed</code> and <code>confirmDismiss</code> callback functions need to be updated.</p>
<p>For both functions, our deletion logic should only be called if the direction is <code>endToStart</code>. If not, we can assume it's <code>startToEnd</code> and include our starring logic.</p>
<pre><code class="lang-dart">Dismissible(
  key: UniqueKey(),
  direction: DismissDirection.horizontal,
  onDismissed: (DismissDirection direction) {
    log(<span class="hljs-string">'Dismissed with direction <span class="hljs-subst">$direction</span>'</span>);
    <span class="hljs-keyword">if</span> (direction == DismissDirection.endToStart) {
      <span class="hljs-comment">// Your deletion logic goes here.</span>
    }
  },
  confirmDismiss: (DismissDirection direction) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (direction == DismissDirection.endToStart) {
      <span class="hljs-keyword">final</span> confirmed = <span class="hljs-keyword">await</span> _confirmDeletion(context);
      log(<span class="hljs-string">'Deletion confirmed: <span class="hljs-subst">$confirmed</span>'</span>);
      <span class="hljs-keyword">return</span> confirmed;
    } <span class="hljs-keyword">else</span> {
      log(<span class="hljs-string">'Starring'</span>);
      <span class="hljs-comment">// The widget is never dismissed in this case. Your star logic goes here.</span>
      setState(() {
        _isStarred = !_isStarred;
      });
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
  },
  background: <span class="hljs-keyword">const</span> ColoredBox(
    color: Colors.red,
    child: Align(
      alignment: Alignment.centerRight,
      child: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Icon(Icons.delete, color: Colors.white),
      ),
    ),
  ),
  child: ListTile(
    leading: <span class="hljs-keyword">const</span> Icon(Icons.flutter_dash),
    title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Dash'</span>),
    subtitle: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Hello!'</span>),
    trailing: Icon(_isStarred ? Icons.star : Icons.star_outline),
  ),
),
</code></pre>
<p>We've updated the message widget to show a filled star if it's starred, or an outlined star if not, and keep track of this with a simple boolean field in the state, just to keep things simple.</p>
<p>One thing to note here is that since we never want to dismiss the widget when we star a message, we always return <code>false</code> in <code>confirmDismiss</code>. Because of this, the <code>onDismissed</code> callback is never called, so our starring logic should be called in <code>confirmDismiss</code>, if the direction is what we expect.</p>
<h3 id="heading-secondary-background">Secondary background</h3>
<p>There's one thing left; the background! We now show the red background with the trash icon when swiping both left and right. For starring, we would like to show an orange background with a star icon, and since we swipe right, the icon should be on the left. The <code>Dismissible</code> widget makes it very easy to have a separate background depending on the swipe direction.</p>
<p>For this case, we're going to have both a <code>background</code> and a <code>secondaryBackground</code>.</p>
<pre><code class="lang-dart">Dismissible(
  ...
  background: <span class="hljs-keyword">const</span> ColoredBox(
    color: Colors.orange,
    child: Align(
      alignment: Alignment.centerLeft,
      child: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Icon(Icons.star, color: Colors.white),
      ),
    ),
  ),
  secondaryBackground: <span class="hljs-keyword">const</span> ColoredBox(
    color: Colors.red,
    child: Align(
      alignment: Alignment.centerRight,
      child: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Icon(Icons.delete, color: Colors.white),
      ),
    ),
  ),
  child: ListTile(...),
),
</code></pre>
<p>Notice that the red deletion background is now actually the <code>secondaryBackground</code>, and the new orange background as the <code>background</code>, because of the directions.</p>
<p>Here is the "swipe to star" action implementation in action:</p>
<center><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655212930011/JeZLaG4gi.gif" alt="swipe_to_star.gif" /></center>

<p>And that is all for the swipe-able message list tile! You can check out the full widget and app code in the source code <a target="_blank" href="https://github.com/dartling/swipe_actions">here</a>.</p>
<h3 id="heading-more-on-the-dismissible-widget">More on the <code>Dismissible</code> widget</h3>
<p>We've gone over a lot of what the <code>Dismissible</code> widget can do, but its constructor accepts a few more optional arguments which we could make use of.</p>
<ul>
<li><code>VoidCallback onResize</code> - A callback function called (multiple times) just before the widget is dismissed, while being resized (contracted). Note: it's actually the background you see being contracted.</li>
<li><code>Duration? resizeDuration</code> - The duration of the resizing/contracting that happens before the widget is dismissed. If you set this to a long enough duration, you can see the background widget slowly being "squeezed up" and disappearing. Set this to <code>null</code> and the background widget doesn't resize; it just stays there.</li>
<li><code>Map&lt;DismissDirection, double&gt; dismissThresholds</code> - This is a useful one. It's a map of directions to a "threshold" (defaults to 0.4), which means that you have to drag a widget at least 40% in a given direction to actually perform the dismissal and the callbacks to be called. So you could raise or reduce the "sensitivity" of your swipe-able widgets.</li>
<li><code>Duration movementDuration</code> - Not to be confused with <code>resizeDuration</code>, this is the duration of the widget sliding back to its place in case dismissal was not confirmed, or if you've already swiped past the threshold and the widget keeps moving on its own.</li>
<li><code>DismissUpdateCallback? onUpdate</code> - Is called while the widget is being dragged. The details include the direction as well as whether the threshold has been reached. A use case for this, mentioned in the documentation, is that you could dynamically change the background of the dismissible widget as soon as the threshold is reached, rather than always displaying it.</li>
</ul>
<h2 id="heading-slide-actions-with-flutterslidable">Slide actions with <code>flutter_slidable</code></h2>
<p>We can now implement swipe-able widgets with different actions depending on the direction. If you want to do even more actions with a swipe, you can try out the <a target="_blank" href="https://pub.dev/packages/flutter_slidable">flutter_slidable</a> package. On swiping/sliding, the background reveals multiple actions that can be done.</p>
<p>We won't go into the implementation details here; the pub.dev page is already quite helpful! But here is what these "slide actions" would look like using the package:</p>
<center><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655212943787/4lngsambK.gif" alt="flutter_slidable.gif" /></center>

<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we showed how to implement swipe actions (or gestures) to swipe to delete or star a message.</p>
<p>We also dug a little deeper into the <code>Dismissible</code> widget and what it can do (but of course the documentation also goes a good job explaining this!).</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/swipe_actions">here</a>.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[How to create a custom plugin in Flutter to call native platform code]]></title><description><![CDATA[Introduction
In this tutorial, we will create a Flutter plugin which targets the Android and iOS platforms, and show how to invoke different methods from Dart, pass arguments of different types, and receive and parse results from the host platforms. ...]]></description><link>https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code</link><guid isPermaLink="true">https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[Flutter SDK]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Tue, 29 Mar 2022 21:27:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648588057078/T8CTqUNUa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial, we will create a Flutter plugin which targets the Android and iOS platforms, and show how to invoke different methods from Dart, pass arguments of different types, and receive and parse results from the host platforms. The platform code won't actually call any real native APIs, but rather return some hard-coded data. But by the end of this tutorial, doing that part should hopefully be easy!</p>
<p>Flutter is mainly a UI framework, and so for a lot platform-specific functionality, we usually use plugins, typically created by the Dart and Flutter community, to achieve things such as <a target="_blank" href="https://pub.dev/packages/battery_plus">getting the current battery level</a>, or <a target="_blank" href="https://pub.dev/packages/flutter_local_notifications">displaying local notifications</a>. However, in some cases, there might not be a plugin already available, or the platform we are targeting might not be supported. In such cases, writing our own custom plugin (or contributing to an existing plugin), might be our only option.</p>
<p>[Updated July 9th 2022] Made changes to reflect the updated plugin templates which use platform interfaces, and added a small section on tests.</p>
<h2 id="heading-starting-with-the-plugin-template">Starting with the plugin template</h2>
<p>To get started, we'll create a Flutter plugin using <code>flutter create</code> with the plugin template.</p>
<pre><code class="lang-sh">flutter create --org dev.dartling --template=plugin --platforms=android,ios app_usage
</code></pre>
<p>This will generate the code for the plugin, as well as an example project that uses this plugin. By default, the generated Android code will be in Kotlin, and iOS in Swift, but you can specify either Java or Objective-C with the <code>-a</code> and <code>-i</code> flags respectively. (<code>-a java</code> and/or <code>-i objc</code>).</p>
<p>Of course, you could target more platforms if you want. For this tutorial, we will only be targeting Android and iOS.</p>
<p>Next, let's take a look at the generated Dart, Kotlin and Swift code after running <code>flutter create</code>.</p>
<h3 id="heading-dart-code">Dart code</h3>
<p>This auto-generated plugin comes with three Dart files. First is <code>AppUsagePlatform</code>, which extends <code>PlatformInterface</code>. This defines the interface, or API, of your plugin.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_platform_interface.dart</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlatform</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">PlatformInterface</span> </span>{
  <span class="hljs-comment">/// <span class="markdown">Constructs a AppUsagePlatform.</span></span>
  AppUsagePlatform() : <span class="hljs-keyword">super</span>(token: _token);

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-built_in">Object</span> _token = <span class="hljs-built_in">Object</span>();

  <span class="hljs-keyword">static</span> AppUsagePlatform _instance = MethodChannelAppUsage();

  <span class="hljs-comment">/// <span class="markdown">The default instance of [AppUsagePlatform] to use.</span></span>
  <span class="hljs-comment">///
  <span class="markdown">/// Defaults to [MethodChannelAppUsage].</span></span>
  <span class="hljs-keyword">static</span> AppUsagePlatform <span class="hljs-keyword">get</span> instance =&gt; _instance;

  <span class="hljs-comment">/// <span class="markdown">Platform-specific implementations should set this with their own</span></span>
  <span class="hljs-comment">/// <span class="markdown">platform-specific class that extends [AppUsagePlatform] when</span></span>
  <span class="hljs-comment">/// <span class="markdown">they register themselves.</span></span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">set</span> instance(AppUsagePlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() {
    <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'platformVersion() has not been implemented.'</span>);
  }
}
</code></pre>
<p>There is a <code>getPlatformVersion</code> which throws an <code>UnimplementedError</code> which we should not touch; it should be overriden by classes extending this one.</p>
<h4 id="heading-platform-interface-and-federated-plugins">Platform interface and federated plugins</h4>
<p><code>PlatformInterface</code> is used to support <a target="_blank" href="https://docs.flutter.dev/development/packages-and-plugins/developing-packages#federated-plugins">federated plugins</a>, which allow to split support of different packages in separate platforms. For example, if we look at the <a target="_blank" href="https://pub.dev/packages/battery_plus"><code>battery_plus</code></a> package, which can be used to access battery information on any platform, has every implementation as a separate package dependency, as well as a dependency on <code>battery_plus_platform_interface</code>.</p>
<p>You can take a look at the documentation if you're interested in learning more about federated plugins, but the general idea is that separate platform implementations can be separate packages and can be maintained independently.</p>
<h4 id="heading-method-channels">Method channels</h4>
<p>The second class is <code>MethodChannelAppUsage</code>, which extends <code>AppUsagePlatform</code>. It's an implementation of our platform interface which uses a <code>MethodChannel</code>. It overrides <code>getPlatformVersion</code> and invokes a specific method in this method channel, which is implemented in both Android and iOS to return the current platform version.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_method_channel.dart</span>
<span class="hljs-comment">/// <span class="markdown">An implementation of [AppUsagePlatform] that uses method channels.</span></span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MethodChannelAppUsage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppUsagePlatform</span> </span>{
  <span class="hljs-comment">/// <span class="markdown">The method channel used to interact with the native platform.</span></span>
  <span class="hljs-meta">@visibleForTesting</span>
  <span class="hljs-keyword">final</span> methodChannel = <span class="hljs-keyword">const</span> MethodChannel(<span class="hljs-string">'app_usage'</span>);

  <span class="hljs-meta">@override</span>
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> version =
        <span class="hljs-keyword">await</span> methodChannel.invokeMethod(<span class="hljs-string">'getPlatformVersion'</span>);
    <span class="hljs-keyword">return</span> version;
  }
}
</code></pre>
<p>The <code>MethodChannel</code> constructor accepts a channel name, which is the name of the channel on which communication between the Dart code and the host platform will happen. This name is typically the plugin name, and this is what the generated code uses, but some plugins usually use a combination of the application package/ID or domain. So for this example we could go for something like <code>dev.dartling.app_usage</code> or <code>dartling.dev/app_usage</code>.</p>
<p>Let's dig a little deeper into  <code>MethodChannel#invokeMethod</code>:</p>
<pre><code class="lang-dart"> Future&lt;T?&gt; invokeMethod&lt;T&gt;(<span class="hljs-built_in">String</span> method, [ <span class="hljs-built_in">dynamic</span> arguments ])
</code></pre>
<p>We can specify which type we expect to be returned by the method channel, and the future's result can always be null. So in the <code>platformVersion</code> getter above, we could be more explicit and use <code>_channel.invokeMethod&lt;String&gt;('getPlatformVersion')</code> instead.</p>
<p>Last but not least, we have the <code>AppUsage</code> class. This is what we'll actually be using in our apps, and in the example app. By default, in the plugin template, there is already an <code>example</code> sub-folder with a Flutter app showing how <code>AppUsage#getPlatformVersion</code> can be used.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsage</span> </span>{
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getPlatformVersion() {
    <span class="hljs-keyword">return</span> AppUsagePlatform.instance.getPlatformVersion();
  }
}
</code></pre>
<p>All this does is call the current <code>instance</code> of <code>AppUsagePlatform</code> and call the appropriate, or in this case the only, method: <code>getPlatformVersion()</code>. In <code>AppUsagePlatform</code>, you can see that <code>instance</code> is actually the implementation using method channels: <code>MethodChannelAppUsage</code>.</p>
<p>To sum up, the 3 classes generated by the plugin template are:</p>
<ul>
<li><code>AppUsagePlatform</code> - A "platform" class which extends <code>PlatformInterface</code>, which defines the API of our plugin (i.e. methods are defined but throw <code>UnimplementedError</code>s). The default instance (returned by the <code>instance</code> getter) of this class returns the default implementation.</li>
<li><code>MethodChannelAppUsage</code> - A class which extends <code>AppUsagePlatform</code> and overrides every unimplemented method in that class. It uses method channels for the method implementations. This is what is returned with <code>AppUsagePlatform.instance</code>.</li>
<li><code>AppUsage</code> - The actual class of our plugin to be used by anyone requiring the plugin's functionality. In the background, this invokes the methods of <code>AppUsagePlatform.instance</code>.</li>
</ul>
<h3 id="heading-kotlin-code-android">Kotlin code (Android)</h3>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsagePlugin.kt</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlugin</span> : <span class="hljs-type">FlutterPlugin</span>, <span class="hljs-type">MethodCallHandler {</span></span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> channel: MethodChannel

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onAttachedToEngine</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> flutterPluginBinding: <span class="hljs-type">FlutterPlugin</span>.<span class="hljs-type">FlutterPluginBinding</span>)</span></span> {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, <span class="hljs-string">"app_usage"</span>)
        channel.setMethodCallHandler(<span class="hljs-keyword">this</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onMethodCall</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> call: <span class="hljs-type">MethodCall</span>, <span class="hljs-meta">@NonNull</span> result: <span class="hljs-type">Result</span>)</span></span> {
        <span class="hljs-keyword">if</span> (call.method == <span class="hljs-string">"getPlatformVersion"</span>) {
            result.success(<span class="hljs-string">"Android <span class="hljs-subst">${android.os.Build.VERSION.RELEASE}</span>"</span>)
        } <span class="hljs-keyword">else</span> {
            result.notImplemented()
        }
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onDetachedFromEngine</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> binding: <span class="hljs-type">FlutterPlugin</span>.<span class="hljs-type">FlutterPluginBinding</span>)</span></span> {
        channel.setMethodCallHandler(<span class="hljs-literal">null</span>)
    }
}
</code></pre>
<p>The <code>onAttachedToEngine</code> and <code>onDetachedFromEngine</code> methods are pretty standard and we won't have to touch them at all. One note about <code>onAttachedToEngine</code>, is the <code>MethodChannel</code> constructor which also accepts a channel name. This name should match whatever we pass in the constructor on the Flutter side of things, so if you decide to go with a different name, make sure you change it in the constructors of all platforms.</p>
<p>The <code>onMethodCall</code> method is where the bulk of the logic happens, and will happen, when we add more functionality to our plugin. This method accepts two parameters. The first, the <code>MethodCall</code>, contains the data we pass from the <code>invokeMethod</code> invocation in Dart. So, <code>MethodCall#method</code> will return the method name (<code>String</code>), and <code>MethodCall#arguments</code> contains any arguments we pass along with the invocation. <code>arguments</code> is an <code>Object</code>, and can be used in different ways, but more on that later.</p>
<p>The <code>Result</code> can be used to return data or errors to Dart. This must <em>always</em> be used, otherwise the <code>Future</code> will never complete, and the invocation would hang indefinitely. With <code>result</code>, we can use the <code>success(Object)</code> method to return any object, <code>error(String errorCode, String errorMessage, Object errorDetails)</code> to return errors, and <code>notImplemented()</code> if the method we are invoking is not implemented (this is what happens in the <code>else</code> block above).</p>
<h3 id="heading-swift-code-ios">Swift code (iOS)</h3>
<pre><code class="lang-swift"><span class="hljs-comment">// SwiftAppUsagePlugin.swift</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SwiftAppUsagePlugin</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">FlutterPlugin</span> </span>{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">register</span><span class="hljs-params">(with registrar: FlutterPluginRegistrar)</span></span> {
    <span class="hljs-keyword">let</span> channel = <span class="hljs-type">FlutterMethodChannel</span>(name: <span class="hljs-string">"app_usage"</span>, binaryMessenger: registrar.messenger())
    <span class="hljs-keyword">let</span> instance = <span class="hljs-type">SwiftAppUsagePlugin</span>()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handle</span><span class="hljs-params">(<span class="hljs-number">_</span> call: FlutterMethodCall, result: @escaping FlutterResult)</span></span> {
    result(<span class="hljs-string">"iOS "</span> + <span class="hljs-type">UIDevice</span>.current.systemVersion)
  }
}
</code></pre>
<p>Note that in the generated Swift code, there are no checks for the <code>getPlatformVersion</code> method name. Let's make some changes to the <code>handle</code> method, to keep things consistent across the two platforms.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handle</span><span class="hljs-params">(<span class="hljs-number">_</span> call: FlutterMethodCall, result: @escaping FlutterResult)</span></span> {
<span class="hljs-keyword">if</span> (call.method == <span class="hljs-string">"getPlatformVersion"</span>) {
    result(<span class="hljs-string">"iOS "</span> + <span class="hljs-type">UIDevice</span>.current.systemVersion)
} <span class="hljs-keyword">else</span> {
    result(<span class="hljs-type">FlutterMethodNotImplemented</span>)
}
</code></pre>
<p>Similarly to Android/Kotlin, <code>FlutterMethodCall</code> has a <code>method</code> string and dynamic <code>arguments</code>. But <code>FlutterResult</code> is a bit different. For a "successful" return, you can just pass any value in <code>result(...)</code>. If the method is not implemented, just pass <code>FlutterMethodNotImplemented</code>, as shown above. And for errors, pass <code>FlutterError.init(code: "ERROR_CODE", message: "error message", details: nil)</code>.</p>
<h2 id="heading-returning-complex-objects">Returning complex objects</h2>
<p>Now that we've seen how the code looks across Dart and our target platforms, let's implement some new functionality. Let's say that our App Usage plugin should return a list of the used apps, showing how much time we spend on each app.</p>
<p>We need to update all 3 classes to support a new method, but all we need to do is add a new method in each class.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_platform_interface.dart</span>
Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'apps has not been implemented.'</span>);
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_method_channel.dart</span>
<span class="hljs-meta">@override</span>
Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt;? usedApps =
      <span class="hljs-keyword">await</span> methodChannel.invokeListMethod&lt;<span class="hljs-built_in">dynamic</span>&gt;(<span class="hljs-string">'getUsedApps'</span>);
  <span class="hljs-keyword">return</span> usedApps?.map(UsedApp.fromJson).toList() ?? [];
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage.dart</span>
Future&lt;<span class="hljs-built_in">List</span>&lt;UsedApp&gt;&gt; <span class="hljs-keyword">get</span> apps {
  <span class="hljs-keyword">return</span> AppUsagePlatform.instance.apps;
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// models.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsedApp</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Duration</span> timeUsed;

  UsedApp(<span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">this</span>.name, <span class="hljs-keyword">this</span>.timeUsed);

  <span class="hljs-keyword">static</span> UsedApp fromJson(<span class="hljs-built_in">dynamic</span> json) {
    <span class="hljs-keyword">return</span> UsedApp(
      json[<span class="hljs-string">'id'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String</span>,
      json[<span class="hljs-string">'name'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String</span>,
      <span class="hljs-built_in">Duration</span>(minutes: json[<span class="hljs-string">'minutesUsed'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">int</span>),
    );
  }
}
</code></pre>
<p>It seems a little tedious to change 3 classes just to support one new method, but at least the idea is simple. The main logic is in <code>MethodChannelAppUsage</code>; in <code>AppUsagePlatform</code> we simply define the method signature and just throw an error, and in <code>AppUsage</code> we simply call the method.</p>
<p>Notice that we actually expect a <code>List&lt;dynamic&gt;</code> rather than <code>List&lt;UsedApp&gt;</code> when we invoke a method from the channel, and map these to <code>UsedApp</code> using the <code>fromJson</code> method. We cannot just cast complex objects, though this will work fine for simple types such as <code>int</code>, <code>double</code>, <code>bool</code> and <code>String</code>. This is because the standard platform channels only support specific data types, which you can read more about <a target="_blank" href="https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-java-tab#codec">here</a>.</p>
<p>Calling <code>_channel.invokeMethod&lt;UsedApp&gt;(...)</code> will result to this error:</p>
<pre><code class="lang-sh">The following _CastError was thrown building MyApp(dirty, state: _MyAppState<span class="hljs-comment">#8bcb2):</span>
<span class="hljs-built_in">type</span> <span class="hljs-string">'_InternalLinkedHashMap&lt;Object?, Object?&gt;'</span> is not a subtype of <span class="hljs-built_in">type</span> <span class="hljs-string">'UsedApp'</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">type</span> cast
</code></pre>
<p>Also notice that we used the convenience <code>invokeListMethod&lt;T&gt;</code>, since we are expecting a list of items to be returned. The above method is equivalent to <code>_channel.invokeMethod&lt;List&lt;dynamic&gt;&gt;(...)</code>. There is also the <code>invokeMapMethod&lt;K, V&gt;</code> if we are expecting a map.</p>
<p>Now, let's implement <code>getUsedApps</code> on the Android and iOS platforms. If we don't, and try to invoke this method from the example app (or any app), we will see this error:</p>
<pre><code class="lang-sh">Unhandled Exception: MissingPluginException(No implementation found <span class="hljs-keyword">for</span> method getUsedApps on channel app_usage)
</code></pre>
<p>For Android, we have to update our <code>onMethodCall</code> function in <code>AppUsagePlugin</code>. We replace the <code>if</code> statement with a <code>when</code>, to make things a bit simpler.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsagePlugin.kt</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsagePlugin</span> : <span class="hljs-type">FlutterPlugin</span>, <span class="hljs-type">MethodCallHandler {</span></span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> appUsageApi = AppUsageApi()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onMethodCall</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> call: <span class="hljs-type">MethodCall</span>, <span class="hljs-meta">@NonNull</span> result: <span class="hljs-type">Result</span>)</span></span> {
        <span class="hljs-keyword">when</span> (call.method) {
            <span class="hljs-string">"getPlatformVersion"</span> -&gt; result.success(<span class="hljs-string">"Android <span class="hljs-subst">${android.os.Build.VERSION.RELEASE}</span>"</span>)
            <span class="hljs-string">"getUsedApps"</span> -&gt; result.success(appUsageApi.usedApps.stream().map { it.toJson() }
                .toList())
            <span class="hljs-keyword">else</span> -&gt; result.notImplemented()
        }
    }
}
</code></pre>
<p>When invoking <code>getUsedApps</code>, we simply use the <code>AppUsageApi</code> to return the used apps, map them to a list of JSON objects (actually just a map of string to a value), and return them with <code>result</code>.</p>
<p>This is what <code>AppUsageApi</code> looks like, if you're curious:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsageApi.kt</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsedApp</span></span>(<span class="hljs-keyword">val</span> id: String, <span class="hljs-keyword">val</span> name: String, <span class="hljs-keyword">val</span> minutesUsed: <span class="hljs-built_in">Int</span>) {
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toJson</span><span class="hljs-params">()</span></span>: Map&lt;String, Any&gt; {
        <span class="hljs-keyword">return</span> mapOf(<span class="hljs-string">"id"</span> to id, <span class="hljs-string">"name"</span> to name, <span class="hljs-string">"minutesUsed"</span> to minutesUsed)
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsageApi</span> </span>{
    <span class="hljs-keyword">val</span> usedApps: List&lt;UsedApp&gt; = listOf(
        UsedApp(<span class="hljs-string">"com.reddit.app"</span>, <span class="hljs-string">"Reddit"</span>, <span class="hljs-number">75</span>),
        UsedApp(<span class="hljs-string">"dev.hashnode.app"</span>, <span class="hljs-string">"Hashnode"</span>, <span class="hljs-number">37</span>),
        UsedApp(<span class="hljs-string">"link.timelog.app"</span>, <span class="hljs-string">"Timelog"</span>, <span class="hljs-number">25</span>),
    )
}
</code></pre>
<p>Just a data class and some hard-coded values. We could have made this simpler and just returned a <code>Map&lt;String, Any&gt;</code> straight from here, but realistically, an API would return its own data classes/models.</p>
<p>Similarly, for iOS, we need to update the <code>handle</code> function in <code>SwiftAppUsagePlugin</code>.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// AppUsageApi</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">UsedApp</span> </span>{
    <span class="hljs-keyword">var</span> id: <span class="hljs-type">String</span>
    <span class="hljs-keyword">var</span> name: <span class="hljs-type">String</span>
    <span class="hljs-keyword">var</span> minutesUsed: <span class="hljs-type">Int</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">toJson</span><span class="hljs-params">()</span></span> -&gt; [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>] {
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">"id"</span>: id,
            <span class="hljs-string">"name"</span>: name,
            <span class="hljs-string">"minutesUsed"</span>: minutesUsed
        ]
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppUsageApi</span> </span>{
    <span class="hljs-keyword">var</span> usedApps = [
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"com.reddit.app"</span>, name: <span class="hljs-string">"Reddit"</span>, minutesUsed: <span class="hljs-number">75</span>),
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"dev.hashnode.app"</span>, name: <span class="hljs-string">"Hashnode"</span>, minutesUsed: <span class="hljs-number">37</span>),
        <span class="hljs-type">UsedApp</span>(id: <span class="hljs-string">"link.timelog.app"</span>, name: <span class="hljs-string">"Timelog"</span>, minutesUsed: <span class="hljs-number">25</span>)
    ]
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// SwiftAppUsagePlugin.swift</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SwiftAppUsagePlugin</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">FlutterPlugin</span> </span>{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> appUsageApi = <span class="hljs-type">AppUsageApi</span>()

  <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handle</span><span class="hljs-params">(<span class="hljs-number">_</span> call: FlutterMethodCall, result: @escaping FlutterResult)</span></span> {
    <span class="hljs-keyword">switch</span> (call.method) {
        <span class="hljs-keyword">case</span> <span class="hljs-string">"getPlatformVersion"</span>:
            result(<span class="hljs-string">"iOS "</span> + <span class="hljs-type">UIDevice</span>.current.systemVersion)
        <span class="hljs-keyword">case</span> <span class="hljs-string">"getUsedApps"</span>:
            result(appUsageApi.usedApps.<span class="hljs-built_in">map</span> { $<span class="hljs-number">0</span>.toJson() })
        <span class="hljs-keyword">default</span>:
            result(<span class="hljs-type">FlutterMethodNotImplemented</span>)
    }
  }
}
</code></pre>
<p>I will not be sharing many snippets from the example app and usages of the <code>AppUsage</code> functions, just to keep the tutorial shorter, but you can take a look at the source code <a target="_blank" href="https://github.com/dartling/app_usage">here</a>. But the actual usage of the plugin is quite simple. We are simply calling the static methods of <code>AppUsage</code> to get data from the host platform, and display it. But in case you're curious to see the method in action, this is how the example app looks like:</p>
<table>
<tr>
<td><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648588947386/x81wxSmPZ.png" alt="android.png" /></td>
<td><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648588469952/DCIGFo5NI.png" alt="ios.png" /></td>
</tr>
</table>

<h2 id="heading-passing-arguments">Passing arguments</h2>
<p>So far, we've shown how to receive data from the host platform. Now what if we want to pass data instead? Let's introduce a new method to our App Usage API. Once again, 3 times!</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_platform_interface.dart</span>
Future&lt;<span class="hljs-built_in">String</span>&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">throw</span> UnimplementedError(<span class="hljs-string">'setAppTimeLimit() has not been implemented.'</span>);
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage_method_channel.dart</span>
<span class="hljs-meta">@override</span>
Future&lt;<span class="hljs-built_in">String</span>&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> result =
        <span class="hljs-keyword">await</span> methodChannel.invokeMethod(<span class="hljs-string">'setAppTimeLimit'</span>, {
      <span class="hljs-string">'id'</span>: appId,
      <span class="hljs-string">'durationInMinutes'</span>: duration.inMinutes,
    });
    <span class="hljs-keyword">return</span> result ?? <span class="hljs-string">'Could not set timer.'</span>;
  } <span class="hljs-keyword">on</span> PlatformException <span class="hljs-keyword">catch</span> (ex) {
    <span class="hljs-keyword">return</span> ex.message ?? <span class="hljs-string">'Unexpected error'</span>;
  }
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// app_usage.dart</span>
Future&lt;<span class="hljs-built_in">String</span>&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) {
  <span class="hljs-keyword">return</span> AppUsagePlatform.instance.setAppTimeLimit(appId, duration);
}
</code></pre>
<p>The difference with the previous method is that we're now also passing <code>parameters</code> to <code>invokeMethod</code>, which is an optional field. While the type of <code>parameters</code> is dynamic, and so could be anything, it's recommended to use a map.</p>
<p>Since our implementation won't actually set any app time limits, it would still be nice to confirm that the host platform has properly received the passed parameters, in our case <code>id</code> and <code>minutes</code>. So to keep things simple, we just want to return a string containing a confirmation that the time limit was set for the given app ID and duration.</p>
<p>Here's the Android/Kotlin implementation:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsageApi.kt</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setTimeLimit</span><span class="hljs-params">(id: <span class="hljs-type">String</span>, durationInMinutes: <span class="hljs-type">Int</span>)</span></span>: String {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Timer of <span class="hljs-variable">$durationInMinutes</span> minutes set for app ID <span class="hljs-variable">$id</span>"</span>;
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-comment">// AppUsagePlugin.kt</span>
<span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onMethodCall</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> call: <span class="hljs-type">MethodCall</span>, <span class="hljs-meta">@NonNull</span> result: <span class="hljs-type">Result</span>)</span></span> {
    <span class="hljs-keyword">when</span> (call.method) {
        ...
        <span class="hljs-string">"setAppTimeLimit"</span> -&gt; result.success(
            appUsageApi.setTimeLimit(
                call.argument&lt;String&gt;(<span class="hljs-string">"id"</span>)!!,
                call.argument&lt;<span class="hljs-built_in">Int</span>&gt;(<span class="hljs-string">"durationInMinutes"</span>)!!
            )
        )
        <span class="hljs-keyword">else</span> -&gt; result.notImplemented()
    }
}
</code></pre>
<p>We get the arguments from the passed parameters using <code>MethodCall#argument</code>, and specify the type we expect the argument to have. This method only works if the parameters passed are either a map or a <code>JSONObject</code>. The method returns an optional result we could be null, hence the <code>!!</code> operator. If the argument for that key in the map is missing or has a different type, an exception is thrown.</p>
<p>Alternatively, we could return the whole map by using:</p>
<pre><code class="lang-kotlin">call.arguments()
</code></pre>
<p>We can also check if the argument exists by using:</p>
<pre><code class="lang-kotlin">call.hasArgument(<span class="hljs-string">"id"</span>) <span class="hljs-comment">// true</span>
call.hasArgument(<span class="hljs-string">"appId"</span>) <span class="hljs-comment">// false</span>
</code></pre>
<p>Next, the iOS/Swift code:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// AppUsageApi</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setTimeLimit</span><span class="hljs-params">(id: String, durationInMinutes: Int)</span></span> -&gt; <span class="hljs-type">String</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Timer of \(durationInMinutes) minutes set for app ID \(id)"</span>
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// SwiftAppUsagePlugin.swift</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handle</span><span class="hljs-params">(<span class="hljs-number">_</span> call: FlutterMethodCall, result: @escaping FlutterResult)</span></span> {
  <span class="hljs-keyword">switch</span> (call.method) {
      ...
      <span class="hljs-keyword">case</span> <span class="hljs-string">"setAppTimeLimit"</span>:
          <span class="hljs-keyword">let</span> arguments = call.arguments <span class="hljs-keyword">as</span>! [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>]
          <span class="hljs-keyword">let</span> id = arguments[<span class="hljs-string">"id"</span>] <span class="hljs-keyword">as</span>! <span class="hljs-type">String</span>
          <span class="hljs-keyword">let</span> durationInMinutes = arguments[<span class="hljs-string">"durationInMinutes"</span>] <span class="hljs-keyword">as</span>! <span class="hljs-type">Int</span>
          result(appUsageApi.setTimeLimit(id: id, durationInMinutes: durationInMinutes))
      <span class="hljs-keyword">default</span>:
          result(<span class="hljs-type">FlutterMethodNotImplemented</span>)
  }
}
</code></pre>
<p>Very similar, but unlike Kotlin, there are no convenience methods to get an argument by its key. Instead, we need to cast <code>call.arguments</code> to a map of <code>String</code> to <code>Any</code>, and then cast each argument to the type we expect it in. Both the arguments and any values in the map can be null, which is why we need the <code>!</code> operator when casting.</p>
<p>And that's it for the platform implementations! In the example app, I've added an icon button which calls this method and displays snackbar with the result string.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// EXAMPLE APP: main.dart</span>
IconButton(
  icon: <span class="hljs-keyword">const</span> Icon(Icons.timer_outlined),
  onPressed: () <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Set duration manually.</span>
    <span class="hljs-keyword">final</span> scaffoldMessenger = ScaffoldMessenger.of(context);
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> result = <span class="hljs-keyword">await</span> _appUsagePlugin.setAppTimeLimit(
        app.id, <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(minutes: <span class="hljs-number">30</span>));
    scaffoldMessenger
        .showSnackBar(SnackBar(content: Text(result)));
  },
)
</code></pre>
<p>Note: we store <code>ScaffoldMessengerState</code> in a variable (<code>scaffoldMessenger</code>) before <code>await</code>ing for the plugin's <code>setAppTimeLimit</code> because we should not use <code>BuildContext</code> across async gaps.</p>
<h2 id="heading-error-handling">Error handling</h2>
<p>We've now learned how to read data returned from the host platform, and pass data to the host platform. For the last part, we'll return an error from the platform side and catch it.</p>
<p>For this example, we'll just be doing this on the Android side. Let's improve the implementation for <code>setAppTimeLimit</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onMethodCall</span><span class="hljs-params">(<span class="hljs-meta">@NonNull</span> call: <span class="hljs-type">MethodCall</span>, <span class="hljs-meta">@NonNull</span> result: <span class="hljs-type">Result</span>)</span></span> {
    <span class="hljs-keyword">when</span> (call.method) {
        ...
        <span class="hljs-string">"setAppTimeLimit"</span> -&gt; {
            <span class="hljs-keyword">if</span> (!call.hasArgument(<span class="hljs-string">"id"</span>) || !call.hasArgument(<span class="hljs-string">"durationInMinutes"</span>)) {
                result.error(
                    <span class="hljs-string">"BAD_REQUEST"</span>,
                    <span class="hljs-string">"Missing 'id' or 'durationInMinutes' argument"</span>,
                    Exception(<span class="hljs-string">"Something went wrong"</span>)
                )
            }
            result.success(
                appUsageApi.setTimeLimit(
                    call.argument&lt;String&gt;(<span class="hljs-string">"id"</span>)!!,
                    call.argument&lt;<span class="hljs-built_in">Int</span>&gt;(<span class="hljs-string">"durationInMinutes"</span>)!!
                )
            )
        }
        <span class="hljs-keyword">else</span> -&gt; result.notImplemented()
    }
</code></pre>
<p>If either the <code>id</code> or <code>durationInMinutes</code> arguments are missing from the method call, we'll throw a more helpful exception. Otherwise, we'd just get a null pointer exception when calling <code>call.argument&lt;T&gt;("key")!!</code>.</p>
<p>This results in a <code>PlatformException</code> being thrown from <code>invokeMethod</code> on the Flutter side. To handle it, we could do the following.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;<span class="hljs-built_in">String</span>&gt; setAppTimeLimit(<span class="hljs-built_in">String</span> appId, <span class="hljs-built_in">Duration</span> duration) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> result = <span class="hljs-keyword">await</span> _channel.invokeMethod(<span class="hljs-string">'setAppTimeLimit'</span>, {
      <span class="hljs-string">'appId'</span>: appId,
      <span class="hljs-string">'durationInMinutes'</span>: duration.inMinutes,
    });
    <span class="hljs-keyword">return</span> result ?? <span class="hljs-string">'Could not set timer.'</span>;
  } <span class="hljs-keyword">on</span> PlatformException <span class="hljs-keyword">catch</span> (ex) {
    <span class="hljs-keyword">return</span> ex.message ?? <span class="hljs-string">'Unexpected error'</span>;
  }
}
</code></pre>
<p>In the snippet above we replaced the <code>id</code> argument with <code>appId</code>, which will lead to a platform exception.</p>
<h2 id="heading-testing">Testing</h2>
<p>The plugin templates already comes with tests for <code>AppUsage</code> as well as <code>MethodChannelAppUsage</code>. No tests necessary for <code>AppUsagePlatform</code> as all it does is throw errors!</p>
<p>They are both nicely set up and easy to extend; the test for <code>AppUsage</code> already provides a mock implementation of <code>AppUsagePlatform</code> and sets it as the default instance, and the test for <code>MethodChannelAppUsage</code> mocks the method call handler's return values.</p>
<p>Take a look at the tests in the <a target="_blank" href="https://github.com/dartling/app_usage/tree/main/test">source code</a> to see how we tested the new methods added for our App Usage plugin.</p>
<h2 id="heading-alternatives">Alternatives</h2>
<p>One not-so-nice aspect of host platform to Flutter communication with <code>MethodChannel</code> is the serialization/deserialization part. When passing many arguments, we need to pass them in a map, and accessing them, casting them, and checking if they exist is not very nice. Same for parsing data returned from the method calls; for this tutorial we needed to map the <code>UsedApp</code> list to a JSON-like map from the Kotlin/Swift code, and then implement a method to create a <code>UsedApp</code> from the returned list on the Flutter side. This can be time-consuming, but also error-prone (all our fields/keys are hard-coded strings and have to be kept in sync across 3 different languages!).</p>
<p>Enter <a target="_blank" href="https://pub.dev/packages/pigeon">Pigeon</a>, an alternative to <code>MethodChannel</code> for Flutter to host platform communication. It is a code generator tool which aims to make this type-safe, easier and faster. With Pigeon, you just have to define the communication interface, and code generation takes care of everything else. In <a target="_blank" href="https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-with-pigeon">this post</a>, we explore using Pigeon as an alternative to method channels and build the exact same functionality as with this tutorial. If you're curious, check it out and see how it compares!</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we created a custom Flutter plugin with both Android and iOS implementations to call (fake) native functionality. We showed how to send and retrieve data from the host platforms, as well as throw errors and handle them.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/app_usage">here</a>.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Displaying a loading overlay or progress HUD in Flutter]]></title><description><![CDATA[💡
Check out my app, Timelog. Built with Flutter!


Introduction
Sometimes, in an app, you want to perform an asynchronous operation and want to prevent the user from tapping/using the app while this operation is in progress. A simple example would b...]]></description><link>https://dartling.dev/displaying-a-loading-overlay-or-progress-hud-in-flutter</link><guid isPermaLink="true">https://dartling.dev/displaying-a-loading-overlay-or-progress-hud-in-flutter</guid><category><![CDATA[Flutter SDK]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Sun, 20 Mar 2022 14:44:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1647786346304/5h9zYjXwY.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Check out my app, <a target="_blank" href="https://timelog.link">Timelog</a>. Built with Flutter!</div>
</div>

<h2 id="heading-introduction">Introduction</h2>
<p>Sometimes, in an app, you want to perform an asynchronous operation and want to prevent the user from tapping/using the app while this operation is in progress. A simple example would be when you create a new item/to-do in your to-do list app. After tapping on "save" or an equivalent button, the save operation would very likely be asynchronous; it might involve storing data to local storage, performing a network request, or both. This is usually a very quick operation, taking less than a second. But sometimes it could take a bit longer. During that time, you don't want users accidentally tapping the save button twice, or changing inputs.</p>
<p>The simplest solution to this would be to display a loading overlay while the operation is in progress. To do this, we're going to need a widget! And in this tutorial, we will do just that.</p>
<h2 id="heading-defining-a-loading-overlay-or-progress-hud">Defining a loading overlay, or progress HUD</h2>
<p>To make sure we're on the same page, this is what the end result will be like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647459020650/cYyC68mLh.png" alt="Loading overlay" /></p>
<p>Just a circular progress indicator with a semi-opaque dark background, covering the current screen and preventing any user input. This is typically called an overlay or a heads-up-display (HUD).</p>
<h2 id="heading-re-inventing-the-wheel">Re-inventing the wheel</h2>
<p>Before we get started, you might not be interested in writing a custom solution for this and would rather use a package for this. If this is the case, there are tens of packages to do just this. Just search for "loading overlay", "loader overlay", or "progress hud" on <a target="_blank" href="https://pub.dev">pub.dev</a>. I haven't used any personally, but <a target="_blank" href="https://pub.dev/packages/loader_overlay">loader_overlay</a> looks well maintained and has a nice API.</p>
<p>At the same time, this is almost trivial to implement, so I'd encourage you to just use your own solution (or copy this one!), and simply adjust it to work for you. Introducing an entirely new package to only do this might be a bit too much.</p>
<h2 id="heading-building-the-overlay-in-2-steps">Building the overlay in 2 steps</h2>
<p>In this tutorial, we will build the loading overlay widget in 3 steps, building on top of the example Flutter counter app. In the first step, we'll simply make the overlay work on the main screen of the app, without any re-usable code. In the second step, we'll extract our overlay code to a new widget that we can re-use anywhere.</p>
<p>If you're just here for the code, just go ahead and <a class="post-section-overview" href="#heading-step-2-extracting-the-loadingoverlay-widget">skip to step #2</a>, or see the code in the repository <a target="_blank" href="https://github.com/dartling/loading_overlay">here</a>.</p>
<h3 id="heading-step-1-using-a-stack">Step #1: Using a <code>Stack</code></h3>
<p>In this tutorial we'll be working with the example Flutter app. If you want to follow along, you can create this with the following command:</p>
<pre><code class="lang-sh">flutter create &lt;app_name&gt;
</code></pre>
<p>The loading overlay widget is actually very simple; it is just the current screen/view/widget, wrapped in a <code>Stack</code>, with the semi-opaque overlay and a centered circular progress indicator at the top of the stack.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Stack(
    children: [
      Scaffold(
        ...
      ),
      <span class="hljs-keyword">const</span> Opacity(
        opacity: <span class="hljs-number">0.8</span>,
        child: ModalBarrier(dismissible: <span class="hljs-keyword">false</span>, color: Colors.black),
      ),
      <span class="hljs-keyword">const</span> Center(
        child: CircularProgressIndicator(),
      ),
    ],
  );
}
</code></pre>
<p>With the change above, we now have a permanent loading overlay in front of the <code>Scaffold</code>. Let's introduce a new state variable, <code>_isLoading</code>, and only show the <code>Opacity</code> and <code>Center</code> widgets if its value is <code>true</code>.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyHomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyHomePage</span>&gt; </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Stack(
      children: [
        Scaffold(
          ...
        ),
        <span class="hljs-keyword">if</span> (_isLoading)
          <span class="hljs-keyword">const</span> Opacity(
            opacity: <span class="hljs-number">0.8</span>,
            child: ModalBarrier(dismissible: <span class="hljs-keyword">false</span>, color: Colors.black),
          ),
        <span class="hljs-keyword">if</span> (_isLoading)
          <span class="hljs-keyword">const</span> Center(
            child: CircularProgressIndicator(),
          ),
      ],
    );
  }
}
</code></pre>
<p>Let's make a small tweak to the <code>_incrementCounter</code> method to test this. When the method is called, we set <code>_isLoading</code> to <code>true</code>, wait 3 seconds, and then set it back to <code>false</code> and increment the counter.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _incrementCounter() <span class="hljs-keyword">async</span> {
  setState(() {
    _isLoading = <span class="hljs-keyword">true</span>;
  });
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">3</span>));
  setState(() {
    _counter++;
    _isLoading = <span class="hljs-keyword">false</span>;
  });
}
</code></pre>
<p>And that's it! Easy, right? Now, this loading overlay code is mixed up with the other view logic, and if we have to push a new view on top of this one (<code>Navigator.push(context, ...)</code>), we'd need to implement this all over again.</p>
<h3 id="heading-step-2-extracting-the-loadingoverlay-widget">Step #2: Extracting the <code>LoadingOverlay</code> widget</h3>
<pre><code class="lang-dart"><span class="hljs-comment">// loading_overlay.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoadingOverlay</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> LoadingOverlay({Key? key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.child}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-keyword">final</span> Widget child;

  <span class="hljs-keyword">static</span> _LoadingOverlayState of(BuildContext context) {
    <span class="hljs-keyword">return</span> context.findAncestorStateOfType&lt;_LoadingOverlayState&gt;()!;
  }

  <span class="hljs-meta">@override</span>
  State&lt;LoadingOverlay&gt; createState() =&gt; _LoadingOverlayState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_LoadingOverlayState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">LoadingOverlay</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</span>;

  <span class="hljs-keyword">void</span> <span class="hljs-keyword">show</span>() {
    setState(() {
      _isLoading = <span class="hljs-keyword">true</span>;
    });
  }

  <span class="hljs-keyword">void</span> <span class="hljs-keyword">hide</span>() {
    setState(() {
      _isLoading = <span class="hljs-keyword">false</span>;
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Stack(
      children: [
        widget.child,
        <span class="hljs-keyword">if</span> (_isLoading)
          <span class="hljs-keyword">const</span> Opacity(
            opacity: <span class="hljs-number">0.8</span>,
            child: ModalBarrier(dismissible: <span class="hljs-keyword">false</span>, color: Colors.black),
          ),
        <span class="hljs-keyword">if</span> (_isLoading)
          <span class="hljs-keyword">const</span> Center(
            child: CircularProgressIndicator(),
          ),
      ],
    );
  }
}
</code></pre>
<p>Notice the <code>LoadingOverlay.of()</code> function, which actually returns <code>_LoadingOverlayState</code>. In the state we expose two public functions, <code>show</code> and <code>hide</code>, which respectively show and hide the loading overlay.</p>
<p>The <code>context.findAncestorStateOfType&lt;T&gt;()</code> function returns any ancestor matching this state class, which can be <code>null</code> if there is no such ancestor. In this example, we can simply assume there will always be one and use <code>!</code>. Or, we could assert it is not <code>null</code> before returning it, throwing an error with a useful message.</p>
<p>Now, in order for <code>_LoadingOverlayState</code> to be an "ancestor" in our app's widget tree, we need to wrap our main view with the <code>LoadingOverlay</code> widget, by making the following changes:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// MyApp</span>
<span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> MaterialApp(
    title: <span class="hljs-string">'Flutter Demo'</span>,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: <span class="hljs-keyword">const</span> LoadingOverlay(
      child: MyHomePage(title: <span class="hljs-string">'Loading Overlay'</span>),
    ),
  );
}
</code></pre>
<p>Note that we reverted the changes initially made in <code>_MyHomePageState</code>, as the <code>Stack</code> and overlay widgets have now been moved to the <code>LoadingOverlay</code> widget.</p>
<p>By simply passing <code>MyHomePage</code> as a child in <code>LoadingOverlay</code>, we can toggle the loading overlay at any point within the <code>MyHomePage</code> widget or any of its child widgets by calling <code>LoadingOverlay.of(context)</code>.</p>
<p>To see our changes in action, let's change the <code>_incrementCounter</code> method again.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _incrementCounter() <span class="hljs-keyword">async</span> {
  LoadingOverlay.of(context).<span class="hljs-keyword">show</span>();
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">3</span>));
  setState(() {
    _counter++;
  });
  LoadingOverlay.of(context).<span class="hljs-keyword">hide</span>();
}
</code></pre>
<p>We now have a reusable <code>LoadingOverlay</code> widget that we can use any time we want to toggle such an overlay. No external packages needed, and all in a stateless widget of less than 50 lines of code.</p>
<p>Note that if we push a new view/widget on top of this one, the context there will not have a <code>LoadingOverlay</code> ancestor. So we'd need to wrap any newly pushed views with another <code>LoadingOverlay</code>.</p>
<pre><code class="lang-dart">Navigator.push(
    context,
    MaterialPageRoute(
        builder: (_) =&gt; <span class="hljs-keyword">const</span> LoadingOverlay(child: NewPage())));
</code></pre>
<p>And that's it for the main part of this tutorial. Next, we will make some small improvements to the <code>LoadingOverlay</code> widget and make it more customizable. After that, in the following step, we will make the widget stateless rather than stateful. If you're not interested, feel free to skip ahead to the <a class="post-section-overview" href="#heading-wrapping-up">end of the post</a>.</p>
<h3 id="heading-improvement-1-tweaks">Improvement #1: Tweaks</h3>
<p>In some cases, the operations you show the loading overlay for might take less than a second, or even half a second. In such cases, you might still want to show an overlay to block user interactions, but not the spinner/circular progress indicator.</p>
<p>We can make a small tweak to our <code>LoadingOverlay</code> widget to delay the circular progress indicator by a duration of our choice, by using a <code>FutureBuilder</code>.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoadingOverlay</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> LoadingOverlay({
    Key? key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.child,
    <span class="hljs-keyword">this</span>.delay = <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>),
  }) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-keyword">final</span> Widget child;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Duration</span> delay;

  ...
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// _LoadingOverlayState</span>
...
Center(
  child: FutureBuilder(
    future: Future.delayed(widget.delay),
    builder: (context, snapshot) {
      <span class="hljs-keyword">return</span> snapshot.connectionState == ConnectionState.done
          ? <span class="hljs-keyword">const</span> CircularProgressIndicator()
          : <span class="hljs-keyword">const</span> SizedBox();
    },
  ),
),
...
</code></pre>
<p>The <code>delay</code> parameter is optional and defaults to 500 milliseconds.</p>
<p>Another change we can do is blur the background when we show the overlay. This can be done, of course, with another widget; <code>BackdropFilter</code> and <code>ImageFilter.blur</code>.</p>
<pre><code class="lang-dart">BackdropFilter(
  filter: ImageFilter.blur(sigmaX: <span class="hljs-number">4.0</span>, sigmaY: <span class="hljs-number">4.0</span>),
  child: <span class="hljs-keyword">const</span> Opacity(
    opacity: <span class="hljs-number">0.8</span>,
    child: ModalBarrier(dismissible: <span class="hljs-keyword">false</span>, color: Colors.black),
  ),
),
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647463062106/RU-kT26gv.png" alt="Blurred overlay" /></p>
<p>Another potential tweak would be to show something else rather than a <code>CircularProgressIndicator</code>. If you want this to vary depending on what you use it for, it could just be an additional <code>Widget</code> parameter that you can simply pass when you create a <code>LoadingOverlay</code>. Or it could even be optional and default to <code>CircularProgressIndicator</code>.</p>
<h3 id="heading-improvement-2-going-stateless">Improvement #2: Going stateless</h3>
<p>I'm a big fan of stateless widgets, and try to avoid stateful widgets when I can. For this loading overlay widget, the only state we need to manage and change is a boolean value that determines whether the overlay is shown or not. Rather than with a stateful widget, we could achieve this by using a <code>ValueNotifier</code> and a <code>ValueListenableBuilder</code>.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoadingOverlayAlt</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  LoadingOverlayAlt({Key? key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.child})
      : _isLoadingNotifier = ValueNotifier(<span class="hljs-keyword">false</span>),
        <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">bool</span>&gt; _isLoadingNotifier;
  <span class="hljs-keyword">final</span> Widget child;

  <span class="hljs-keyword">static</span> LoadingOverlayAlt of(BuildContext context) {
    <span class="hljs-keyword">return</span> context.findAncestorWidgetOfExactType&lt;LoadingOverlayAlt&gt;()!;
  }

  <span class="hljs-keyword">void</span> <span class="hljs-keyword">show</span>() {
    _isLoadingNotifier.value = <span class="hljs-keyword">true</span>;
  }

  <span class="hljs-keyword">void</span> <span class="hljs-keyword">hide</span>() {
    _isLoadingNotifier.value = <span class="hljs-keyword">false</span>;
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ValueListenableBuilder&lt;<span class="hljs-built_in">bool</span>&gt;(
      valueListenable: _isLoadingNotifier,
      child: child,
      builder: (context, isLoading, child) {
        <span class="hljs-keyword">return</span> Stack(
          children: [
            child!,
            <span class="hljs-keyword">if</span> (isLoading)
              <span class="hljs-keyword">const</span> Opacity(
                opacity: <span class="hljs-number">0.8</span>,
                child: ModalBarrier(dismissible: <span class="hljs-keyword">false</span>, color: Colors.black),
              ),
            <span class="hljs-keyword">if</span> (isLoading)
              <span class="hljs-keyword">const</span> Center(
                child: CircularProgressIndicator(),
              ),
          ],
        );
      },
    );
  }
}
</code></pre>
<p>We simply initialize the value notifier with a default <code>false</code> value, and expose methods that update its value. The <code>ValueListenableBuilder</code> will be rebuilt every time the notifier's value changes, so we end up with the same result as doing a <code>setState</code> with a stateful widget.</p>
<p>Note that we now use <code>context.findAncestorWidgetOfExactType&lt;T&gt;()</code> instead of <code>context.findAncestorStateOfType&lt;T&gt;()</code>, as the widget is now stateless, and the <code>show</code> and <code>hide</code> methods are part of this stateless widget.</p>
<p>Surprisingly (for me, at least!), switching from a stateful to a stateless widget does not really decrease the lines of code in the widget by a lot. And since we initialize the <code>ValueNotifier</code> in the constructor, the constructor can no longer be constant. So in the end, this might be less of an improvement but more of a matter of preference.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we created a loading overlay widget and implemented functionality to easily display and hide it, without external packages and less than 70 lines of code. Without the spinner delay and blur filter tweaks, this is even less, at around 50 lines.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/loading_overlay">here</a>.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Implementing True Black dark theme mode in Flutter]]></title><description><![CDATA[Introduction
If you've been using Flutter for a while, you already know theming with Flutter is very easy, and switching between light and dark mode is available out-of-the-box, something which can be a pain to implement with other frameworks if you ...]]></description><link>https://dartling.dev/implementing-true-black-dark-theme-mode-in-flutter</link><guid isPermaLink="true">https://dartling.dev/implementing-true-black-dark-theme-mode-in-flutter</guid><category><![CDATA[Flutter SDK]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[Dart]]></category><category><![CDATA[theme]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Mon, 24 Jan 2022 18:11:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655214883037/syU-mIJT_.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>If you've been using Flutter for a while, you already know theming with Flutter is very easy, and switching between light and dark mode is available out-of-the-box, something which can be a pain to implement with other frameworks if you don't build your app with this in mind.</p>
<p>But what if light and dark mode is not enough for you? What if you want to implement a "true black", "true dark", "lights out" or "OLED dark mode", or whatever the proper name for this extra mode is...?</p>
<p>This is not trivial to do, but if you really wanted, you could configure <code>ThemeData</code> to have darker colors and use the "true" black color (<code>Color(0xFF000000)</code>, or <code>Colors.black</code> in the material library). Theming in Flutter is easy, after all!</p>
<p>But there's a simpler, even easier way to implement this mode. Enter <code>flex_color_scheme</code>.</p>
<h2 id="heading-the-flexcolorscheme-package">The <code>flex_color_scheme</code> package</h2>
<p><a target="_blank" href="https://pub.dev/packages/flex_color_scheme"><code>flex_color_scheme</code></a> is a package that allows you to easily build color scheme based Flutter themes. It has lots of beautiful built-in themes available, but you can also easily create your own by specifying a primary color and an optional secondary color. With this package, enabling "true black" mode is as simple as setting a value to <code>true</code>.</p>
<h3 id="heading-installing-the-package">Installing the package</h3>
<p>Run this in your terminal to install the package:</p>
<pre><code class="lang-sh">flutter pub add flex_color_scheme
</code></pre>
<p>Or just add the dependency in <code>pubspec.yaml</code>:</p>
<pre><code class="lang-sh">dependencies:
  flex_color_scheme: ^4.1.1
</code></pre>
<p>As of the time of writing of this post, the current version is <code>4.1.1</code>.</p>
<h3 id="heading-using-the-package">Using the package</h3>
<p>Now, let's set up a theme with <code>flex_color_scheme</code>. For this tutorial, I'll be using the default Flutter counter starter app.</p>
<p>Let's apply a new theme by updating the main <code>build</code> method:</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> MaterialApp(
    title: <span class="hljs-string">'Theming Tutorial'</span>,
    theme: FlexThemeData.light(scheme: FlexScheme.hippieBlue),
    darkTheme: FlexThemeData.dark(scheme: FlexScheme.hippieBlue),
    themeMode: ThemeMode.dark,
    home: <span class="hljs-keyword">const</span> MyHomePage(),
  );
}
</code></pre>
<p>I'm a fan of the built-in Hippie Blue scheme, so I'll be using that one. <code>FlexThemeData</code> provides convenience extensions to return a <code>ThemeData</code> object given a scheme or colors.</p>
<p>You can specify your own colors which will override the scheme colors by using the <code>FlexSchemeColor#of</code> constructor and specifying a primary and an optional secondary color. The default scheme is <code>FlexScheme.material</code>.</p>
<pre><code class="lang-dart">FlexThemeData.light(colors: FlexSchemeColor.from(primary: Colors.blue, secondary: Colors.green))
</code></pre>
<p>Below is what our app looks like right now. Strangely, there is no blue in "hippie blue" dark mode! This is because, though the app bar is actually blue (primary color) in light mode, it's black in dark mode, as per the Material guidelines.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1642791552523/0_BrXYNhe.png" alt="Dark mode with flex color scheme" /></p>
<h2 id="heading-true-black-mode">True black mode</h2>
<p>Switching from dark mode to true black mode is easy. Simply set the <code>darkIsTrueBlack</code> in your flex theme data to <code>true</code>.</p>
<pre><code class="lang-dart">darkTheme: FlexThemeData.dark(
  scheme: FlexScheme.hippieBlue,
  darkIsTrueBlack: <span class="hljs-keyword">true</span>,
),
</code></pre>
<p>Save your change, watch your app hot-reload, and... ta-da! Dark mode is now true black. On OLED screens, this can also preserve battery.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1642791801795/8Zwe7JMj4.png" alt="True black mode with flex color scheme" /></p>
<p>What does this flag actually do? It sets the scaffold background to black, as well as making other surfaces 8% darker.</p>
<p>Note: the <code>darkIsTrueBlack</code> flag is only available in <code>FlexThemeData#dark</code>. There is a <code>lightIsTrueWhite</code> equivalent in <code>FlexThemeData#light</code>, but is not as useful. Similarly to the dark flag, it sets the scaffold background to white, and other surfaces 8% lighter.</p>
<h2 id="heading-supporting-both-dark-and-true-black-mode">Supporting both dark and true black mode</h2>
<p>So far, we've shown how to easily set up "true black" mode in our Flutter app. If that's all you're here for, feel free to skip this and jump to the conclusion!</p>
<p>But what if you'd like to support both "regular" dark mode, as well as "true black"? If your app has plenty of users, surely there will be preferences for both. Some users might find "true black" too dark, while others will want to take advantage of their device's OLED screen and use true black.</p>
<p>To support theme changing in the app, it's time to manage some state!</p>
<h3 id="heading-switching-between-light-and-dark-theme-mode">Switching between light and dark theme mode</h3>
<p>Before adding this third theme option, we should note that there is an additional theme mode: <code>ThemeMode.system</code>. This will use the user's system theme: it may be light or dark. To get started, let's implement the option to switch between these 3 theme modes (<code>light</code>, <code>dark</code>, and <code>system</code>) in our app.</p>
<p>To implement this, I will use a <code>ValueNotifier</code>, along with a <code>ValueListenableBuilder</code>, just to keep things simple. You could instead do this with e.g. a <code>ThemeController</code> with the <code>ChangeNotifier</code> mixin, which could additionally store the user's selection to local storage (e.g. <code>shared_preferences</code>). Or, you could use a state management package of your choice. But this is outside the scope of this tutorial!</p>
<p>This is what our root widget looks like now:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// This could be loaded from storage e.g. with `shared_preferences`</span>
  <span class="hljs-keyword">const</span> themeMode = ThemeMode.system;
  runApp(MyApp(theme: themeMode));
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  MyApp({Key? key, <span class="hljs-keyword">required</span> ThemeMode theme})
      : _themeNotifier = ValueNotifier(theme),
        <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-keyword">final</span> ValueNotifier&lt;ThemeMode&gt; _themeNotifier;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ValueListenableBuilder&lt;ThemeMode&gt;(
      valueListenable: _themeNotifier,
      child: MyHomePage(onThemeUpdate: onThemeUpdate),
      builder: (context, theme, child) {
        <span class="hljs-keyword">return</span> MaterialApp(
          title: <span class="hljs-string">'Theming Tutorial'</span>,
          theme: FlexThemeData.light(scheme: FlexScheme.hippieBlue),
          darkTheme: FlexThemeData.dark(
            scheme: FlexScheme.hippieBlue,
            darkIsTrueBlack: <span class="hljs-keyword">true</span>,
          ),
          themeMode: theme,
          home: child,
        );
      },
    );
  }

  <span class="hljs-keyword">void</span> onThemeUpdate(ThemeMode themeMode) {
    _themeNotifier.value = themeMode;
  }
}
</code></pre>
<p>We wrapped our <code>MaterialApp</code> with a <code>ValueListenableBuilder</code>. The value listenable of this builder is a <code>ValueNotifier</code>, initialized with the theme mode we passed when creating the root widget.</p>
<p>Every time we want to change the theme, we will change the value of <code>_themeNotifier</code>, which will trigger the builder and rebuild our <code>MaterialApp</code> widget.</p>
<p>Notice that we're now passing the function that will change the notifier's value down to the home page widget. Whenever we want to change the theme, we can use this function.</p>
<p>To change the theme, let's add a new action in the app bar.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">return</span> Scaffold(
  appBar: AppBar(
    title: Text(widget.title),
    actions: [
      IconButton(
        onPressed: _selectTheme,
        icon: <span class="hljs-keyword">const</span> Icon(Icons.brightness_4_outlined),
      )
    ],
  ),
  ...
);

Future&lt;<span class="hljs-keyword">void</span>&gt; _selectTheme() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> theme = <span class="hljs-keyword">await</span> _showThemePicker();
  <span class="hljs-keyword">if</span> (theme != <span class="hljs-keyword">null</span>) {
    widget.onThemeUpdate(theme);
  }
}

Future&lt;ThemeMode?&gt; _showThemePicker() {
  <span class="hljs-keyword">return</span> showModalBottomSheet&lt;ThemeMode&gt;(
    context: context,
    builder: (context) {
      <span class="hljs-keyword">return</span> SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Light'</span>),
              leading: <span class="hljs-keyword">const</span> Icon(Icons.brightness_7_outlined),
              onTap: () {
                Navigator.pop(context, ThemeMode.light);
              },
            ),
            ListTile(
              title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Dark'</span>),
              leading: <span class="hljs-keyword">const</span> Icon(Icons.brightness_2_outlined),
              onTap: () {
                Navigator.pop(context, ThemeMode.dark);
              },
            ),
            ListTile(
              title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'System'</span>),
              leading: <span class="hljs-keyword">const</span> Icon(Icons.settings_outlined),
              onTap: () {
                Navigator.pop(context, ThemeMode.system);
              },
            ),
          ],
        ),
      );
    },
  );
}
</code></pre>
<p>Lots of duplicate code, but I'll leave the refactoring up to you if you plan to use the same approach to theme changing!</p>
<p>When we tap on the "brightness" icon in the app bar, we are shown a modal bottom sheet with our 3 choices. By tapping on any of the themes, we call our function which updates the value of the <code>ValueNotifier</code>. This causes the app to rebuild, and we can see the theme changing.</p>
<p>Note 1: <code>showModalBottomSheet</code> can always also return <code>null</code>, for example when the user taps outside the modal or presses the back button. If it does, we don't do anything.</p>
<p>Note 2: We use <code>SafeArea</code> to wrap the body of the modal. This avoids intrusions by the operating system. For example, since this is a bottom sheet, a phone's notch could be hiding its contents, but wrapping the bottom sheet in a <code>SafeArea</code> widget will give it sufficient padding to avoid this (but only if there is a notch!).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1642792138723/BFSRKKWxk.png" alt="Modal to switch between theme modes" /></p>
<h3 id="heading-switching-between-light-dark-and-true-black-mode">Switching between light, dark, and true black mode</h3>
<p>Now, to support a fourth option, true black mode, we can't keep using <code>ThemeMode</code>. Let's introduce a new <code>ThemeOption</code> enum.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> ThemeOption { light, dark, system, trueBlack }

<span class="hljs-keyword">extension</span> ThemeOptionUtils <span class="hljs-keyword">on</span> ThemeOption {
  ThemeMode <span class="hljs-keyword">get</span> themeMode {
    <span class="hljs-keyword">switch</span> (<span class="hljs-keyword">this</span>) {
      <span class="hljs-keyword">case</span> ThemeOption.light:
        <span class="hljs-keyword">return</span> ThemeMode.light;
      <span class="hljs-keyword">case</span> ThemeOption.dark:
      <span class="hljs-keyword">case</span> ThemeOption.trueBlack:
        <span class="hljs-keyword">return</span> ThemeMode.dark;
      <span class="hljs-keyword">case</span> ThemeOption.system:
        <span class="hljs-keyword">return</span> ThemeMode.system;
    }
  }
}
</code></pre>
<p><code>ThemeOption.trueBlack</code> will still map to <code>ThemeMode.dark</code>, but we'll get to this in a bit.</p>
<p>Let's also extract all the themes to a separate class, just to keep things simpler.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// app_theme.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppTheme</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _scheme = FlexScheme.hippieBlue;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> light = FlexThemeData.light(scheme: _scheme);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> dark = FlexThemeData.dark(scheme: _scheme);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> trueBlack =
      FlexThemeData.dark(scheme: _scheme, darkIsTrueBlack: <span class="hljs-keyword">true</span>);
}
</code></pre>
<p>Now, all that's left is to simply switch our value notifier from <code>ThemeMode</code> to <code>ThemeOption</code>. This is quite an easy change so I won't share the whole code as it's identical to the snippets share previously, but you can <a target="_blank" href="https://github.com/dartling/theming">check the source code out on GitHub</a>.</p>
<p>The only additional changes were the extra "true black" option in the theme picker, as well as the way the theme in <code>MaterialApp</code> is determined.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// _MyHomePageState#_showThemePicker</span>
...
ListTile(
  title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'True black'</span>),
  leading: <span class="hljs-keyword">const</span> Icon(Icons.brightness_1_outlined),
  onTap: () {
    Navigator.pop(context, ThemeOption.trueBlack);
  },
),
...
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// MyApp</span>
<span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> ValueListenableBuilder&lt;ThemeOption&gt;(
    valueListenable: _themeNotifier,
    child: MyHomePage(onThemeUpdate: onThemeUpdate),
    builder: (context, themeOption, child) {
      <span class="hljs-keyword">return</span> MaterialApp(
        title: <span class="hljs-string">'Theming Tutorial'</span>,
        theme: AppTheme.light,
        darkTheme: themeOption == ThemeOption.trueBlack
            ? AppTheme.trueBlack
            : AppTheme.dark,
        themeMode: themeOption.themeMode,
        home: child,
      );
    },
  );
}
</code></pre>
<p>Very similar to what we had before, except now that the value listenable's value is of type <code>ThemeOption</code>, we need to map the option to <code>ThemeMode</code> to pass it as a <code>MaterialApp</code> argument. And the main change, to support true black mode, is to check whether the option is <code>trueBlack</code>, and if it is, use the appropriate <code>darkTheme</code>. And that's it! We can now choose between 4 different options.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1642792333850/E43YJ_3x4.png" alt="Modal to switch between theme modes as well as True Black mode" /></p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this tutorial, we showed how to easily enable true black mode in a Flutter app with the <code>flex_color_scheme</code> package. </p>
<p>We also showed how to support both dark and true black modes (in addition to light mode, of course!) in the same app.</p>
<p>You can find the full source code <a target="_blank" href="https://github.com/dartling/theming">here</a>.</p>
<p>I am typically against introducing a new dependency just to introduce a small new feature. However, there is much more to <code>flex_color_scheme</code> than true black mode! I encourage you to take a look at the <a target="_blank" href="https://pub.dev/packages/flex_color_scheme/example">example</a>, and dig deeper into the package.</p>
<p>If you found this helpful and would like to be notified of any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Going full-stack with Flutter and Supabase - Part 2: Database]]></title><description><![CDATA[Introduction
In the previous part of this tutorial series, we went over what Supabase is, and started building a simple notes app with Supabase. So far, we got authentication working.
In this part, we will go over Supabase's Database offering. We wil...]]></description><link>https://dartling.dev/full-stack-with-flutter-and-supabase-pt-2-database</link><guid isPermaLink="true">https://dartling.dev/full-stack-with-flutter-and-supabase-pt-2-database</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter SDK]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Mon, 24 May 2021 15:22:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1621868415525/HJQD8oDrT.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In the <a target="_blank" href="https://dartling.dev/full-stack-with-flutter-and-supabase-pt-1-authentication">previous part</a> of this tutorial series, we went over what Supabase is, and started building a simple notes app with Supabase. So far, we got authentication working.</p>
<p>In this part, we will go over Supabase's Database offering. We will create a table to hold all the users' notes, and display them in the app, as well as let users create, edit and delete notes.</p>
<p>Code snippets for both widgets and services will be shared along the tutorial, but you can find the full source code <a target="_blank" href="https://github.com/dartling/supanotes/tree/pt2-database">here</a>.</p>
<h2 id="heading-postgres-and-supabase">Postgres and Supabase</h2>
<p>Supabase is built on top of Postgres, a relational database. Some basic knowledge of relational databases and SQL would help, but is not required, as Supabase makes it easy to view the database, its tables, and run queries through its <a target="_blank" href="https://app.supabase.io/">admin interface</a>.</p>
<p>We'll be making use of the SQL editor on Supabase's admin interface to run the SQL queries to create the tables we need.</p>
<p>Supabase also provides a nice "table editor" which lets you create tables without needing to write SQL. However, for the tutorial, it's actually easier to share SQL queries to run directly from the SQL editor. If you'll be working with Supabase, I'd recommend getting familiar with SQL anyway.</p>
<p>With the Supabase client, we can access the database with two ways:</p>
<ul>
<li>RESTful API - This uses <a target="_blank" href="https://postgrest.org/en/stable/">PostgREST</a> behind the scenes, which is a thin API layer on top of Postgres, which allows you to write your queries straight from the client side of the code (our Flutter app). This is what we'll be using in this tutorial.</li>
<li>Realtime API - You can also listen to database changes over WebSockets with Supabase's Realtime server. We won't be using this in this tutorial.</li>
</ul>
<h3 id="heading-tables">Tables</h3>
<p>A table is a collection of structured data that lives in the Postgres database. When using Supabase's authentication, when a user signs up, the user data is stored as a row in a <code>users</code> table. Each row has several columns, containing information such as email, creation date, etc. Note that the <code>users</code> table managed by Supabase is off-limits and cannot be accessed from the client. If we want to store additional user data, we'd need to create a new, separate table.</p>
<p>A table column has a type restriction: it can be a number, text, or something else. Once we create a table, this is not straight-foward to change, so we should think of our data model carefully first. But we can always add more columns later!</p>
<p>Each table has a column that is a primary key, a unique identifier which we can use to update or fetch specific rows from the table.</p>
<p>There are lots of useful resources on SQL, Postgres and relational databases out there if you're not familiar/comfortable with the concepts, but this tutorial should be easy to follow either way.</p>
<h3 id="heading-data-types">Data types</h3>
<p>Postgres supports several <a target="_blank" href="https://www.postgresql.org/docs/9.5/datatype.html">data types</a>, but we'll mostly be using <code>integer</code> and <code>varchar</code>/<code>text</code>. Once we create a table with columns of these data types, this schema is enforced and we cannot, for example, insert a string value in an <code>integer</code> column.</p>
<p>Another notable data type is Postgres' <code>jsonb</code>, which allows you to store JSON strings containing multiple fields of your data in a column. This is very flexible and useful if we're not sure how our data is going to look like in the beginning. You could start with a <code>jsonb</code> column and eventually move to something more structured. However, while it's more flexible, there's also no enforced schema on the data stored inside the column, so if you know what data you'll be storing in advance, it's recommended to just use multiple columns for each field.</p>
<p>One good use case of the <code>jsonb</code> data type would be if you're migrating from a non-relational database (e.g. Firebase) to Supabase. You could map all your documents to a row containing an ID (the primary key), and a <code>jsonb</code> column containing all the document data in JSON.</p>
<h2 id="heading-part-2-database">Part 2: Database</h2>
<h3 id="heading-creating-our-first-database">Creating our first database</h3>
<p>The first table we'll be needing for this note app, is a <code>notes</code> table! For starters, our notes will have a title, optional content, and creation and modification timestamps. Since we're building a Flutter app, let's start with the Dart model first.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// models/note.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Note</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> content;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> createTime;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> modifyTime;

  Note(<span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">this</span>.title, <span class="hljs-keyword">this</span>.content, <span class="hljs-keyword">this</span>.createTime, <span class="hljs-keyword">this</span>.modifyTime);
}
</code></pre>
<p>We also need an <code>id</code> field so we can edit and delete notes.</p>
<p>Now that we have the model, let's create a table in Supabase. From the admin interface, select your project, and then the SQL editor tab. On the top left you should see a "New query" option. We can pass raw SQL queries in this editor and run them against our database. We'll do this for creating tables, and can also do this when having to quickly check out some data, or run some migrations.</p>
<p>Let's run the following query to create the <code>notes</code> table.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> notes (
  <span class="hljs-keyword">id</span> bigserial primary <span class="hljs-keyword">key</span>,
  title <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">content</span> <span class="hljs-built_in">text</span>,
  create_time timestamptz <span class="hljs-keyword">default</span> <span class="hljs-keyword">now</span>() <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  modify_time timestamptz <span class="hljs-keyword">default</span> <span class="hljs-keyword">now</span>() <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  user_id <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">references</span> auth.users (<span class="hljs-keyword">id</span>) <span class="hljs-keyword">default</span> auth.uid() <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
);
</code></pre>
<p>Our first table is ready! In the Database tab, you can now see the <code>notes</code> table in the <code>public</code> schema.</p>
<p>The <code>id</code> field is a <code>bigserial</code>, which is a <code>bigint</code> that is generated automatically if left blank, and the values are incremented automatically. The timestamp fields default to the current time.</p>
<p>Every field is required except <code>content</code>, which is optional. The <code>user_id</code> field is not part of our model, but we need this on the database side as we'll need this field so we can return the correct data when fetching notes for a specific user.</p>
<p>The <code>user_id</code> column has a <a target="_blank" href="https://www.postgresql.org/docs/9.2/ddl-constraints.html">foreign key constraint</a> and it references <code>auth.users (id)</code>. This means each note created needs to have a <code>user_id</code> field that matches an <code>id</code> field in the <code>users</code> table in the <code>auth</code> schema in our database. If the user ID provided does not match an existing user, we won't be able to insert this note to our database.</p>
<p>The <code>user_id</code> value defaults to <code>auth.uid()</code>, which is a special function in Postgres, provided by Supabase, that returns the current user by extracting it from the JSON web token (discussed in the previous part about authentication).</p>
<p>The <code>auth</code> schema is used by Supabase for authentication. All tables we create for the notes app will be created in a separate <code>public</code> schema.</p>
<p>Note that we could omit the <code>(id)</code> in the SQL query above, as if there is no column specified as reference the primary key of that table is used, which in our case is <code>id</code>.</p>
<h3 id="heading-fetching-data">Fetching data</h3>
<p>Now we want to fetch the notes of the currently signed in user to display in the app. We can do this directly from the Supabase client, which uses <a target="_blank" href="https://postgrest.org/en/stable/">PostgREST</a> behind the scenes.</p>
<p>Let's create a <code>NotesService</code> which will use the Supabase client to fetch the notes, and map them to our <code>Note</code> model.</p>
<p>Here is our updated <code>Services</code> inherited widget which will also now contain the <code>NotesService</code> so we can retrieve it from any widgets that need it.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Services</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">InheritedWidget</span> </span>{
  <span class="hljs-keyword">final</span> AuthService authService;
  <span class="hljs-keyword">final</span> NotesService notesService;

  Services._({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.authService,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.notesService,
    <span class="hljs-keyword">required</span> Widget child,
  }) : <span class="hljs-keyword">super</span>(child: child);

  <span class="hljs-keyword">factory</span> Services({<span class="hljs-keyword">required</span> Widget child}) {
    <span class="hljs-keyword">final</span> client = SupabaseClient(supabaseUrl, supabaseKey);
    <span class="hljs-keyword">final</span> authService = AuthService(client.auth);
    <span class="hljs-keyword">final</span> notesService = NotesService(client);
    <span class="hljs-keyword">return</span> Services._(
      authService: authService,
      notesService: notesService,
      child: child,
    );
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> updateShouldNotify(InheritedWidget oldWidget) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
  }

  <span class="hljs-keyword">static</span> Services of(BuildContext context) {
    <span class="hljs-keyword">return</span> context.dependOnInheritedWidgetOfExactType&lt;Services&gt;()!;
  }
}
</code></pre>
<p>And here is the <code>NotesService</code>:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotesService</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> notes = <span class="hljs-string">'notes'</span>;

  <span class="hljs-keyword">final</span> SupabaseClient _client;

  NotesService(<span class="hljs-keyword">this</span>._client);

  Future&lt;<span class="hljs-built_in">List</span>&lt;Note&gt;&gt; getNotes() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.from(notes).select().execute();
    <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> results = response.data <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt;;
      <span class="hljs-keyword">return</span> results.map((e) =&gt; toNote(e)).toList();
    }
    log(<span class="hljs-string">'Error fetching notes: <span class="hljs-subst">${response.error!.message}</span>'</span>);
    <span class="hljs-keyword">return</span> [];
  }

  Note toNote(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; result) {
    <span class="hljs-keyword">return</span> Note(
      result[<span class="hljs-string">'id'</span>],
      result[<span class="hljs-string">'title'</span>],
      result[<span class="hljs-string">'content'</span>],
      <span class="hljs-built_in">DateTime</span>.parse(result[<span class="hljs-string">'create_time'</span>]),
      <span class="hljs-built_in">DateTime</span>.parse(result[<span class="hljs-string">'modify_time'</span>]),
    );
  }
}
</code></pre>
<p>By using <code>select()</code>, we return all columns of the notes table in the results. Since we actually don't need the <code>user_id</code> field, we can select only the fields we actually need.</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-built_in">List</span>&lt;Note&gt;&gt; getNotes() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client
      .from(notes)
      .select(<span class="hljs-string">'id, title, content, create_time, modify_time'</span>)
      .execute();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">final</span> results = response.data <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">return</span> results.map((e) =&gt; toNote(e)).toList();
  }
  log(<span class="hljs-string">'Error fetching notes: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> [];
}
</code></pre>
<p>This query selects all notes, for all users. It would make sense for our query to have a <code>where</code> clause with the condition that the <code>user_id</code> field of the note matches the one of the currently signed in user. However, this query is run from the client, NOT the server. Which means, potentially, one could accidentally (or not) fetch notes of other users. This is one of the drawbacks of not having a back-end server, as all the data is available to all users by default. This is a huge issue, but the solution is actually pretty simple. Enter Policies.</p>
<h3 id="heading-policies">Policies</h3>
<p>Policies are a a PostgreSQL feature that allows you to define row level security policies for tables. This means, that even with the ability to run any query for a table, no rows will be visible or updatable unless the policy allows it.</p>
<p>To understand this better, let's create a policy:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Users can only view their own notes"</span>
<span class="hljs-keyword">on</span> notes
<span class="hljs-keyword">for</span> <span class="hljs-keyword">select</span>
<span class="hljs-keyword">using</span> (auth.uid() = user_id);
</code></pre>
<p>This is a SQL query, which you can run from Supabase's admin interface. Now let's dig into the query. We're creating a policy with a useful description, on the notes table. This policy is for <code>select</code> statements. The condition for this policy, is that <code>auth.uid()</code> is the same as the <code>user_id</code> field of a note. As mentioned before, <code>auth.uid()</code> always returns the currently signed in user's ID.</p>
<p>This means, that even if we no longer filter on the user ID to fetch the notes for a user, only the notes of the current user will be returned. It's basically an automatic <code>where</code> clause! That's quite powerful, since if you set up your policies right, you don't have to worry about accidentally exposing data to the wrong users, or users updating or deleting data that is not their own.</p>
<p>Now, we've restricted users from <code>select</code>ing other users' notes, but what about other commands? There is still <code>insert</code>, <code>update</code>, and <code>delete</code>. But we can also use <code>all</code> for a policy, which will apply it to all commands.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Users can only view and update their own notes"</span>
<span class="hljs-keyword">on</span> notes
<span class="hljs-keyword">for</span> <span class="hljs-keyword">all</span>
<span class="hljs-keyword">using</span> (auth.uid() = user_id)
</code></pre>
<p>That should do it! With this policy, we no longer need the previous one, as it's already covered by <code>all</code>.</p>
<h3 id="heading-displaying-notes">Displaying notes</h3>
<p>Now that our <code>getNotes()</code> function returns all notes for the current user, let's update our <code>NotesPage</code> widget to display them.</p>
<p>Just for testing, if you're curious if this is working as intended, we can add a note manually through a SQL query. Feel free to skip this part though, since next up we'll add functionality to actually create new notes.</p>
<p>To create a note manually, let's get our user's ID from the database with the following query.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">from</span> auth.users;
</code></pre>
<p>And let's insert a note for this user.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> notes <span class="hljs-keyword">values</span> (<span class="hljs-number">1</span>, <span class="hljs-string">'My note'</span>, <span class="hljs-string">'Note content'</span>, <span class="hljs-keyword">now</span>(), <span class="hljs-keyword">now</span>(), <span class="hljs-string">'2cb4a80a-3c75-4d80-86b0-d76a094cf915'</span>)
</code></pre>
<p>You could also add a note without needing SQL through the table editor.</p>
<p>Here is the updated <code>NotesPage</code> widget:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotesPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> NotesPage();

  <span class="hljs-meta">@override</span>
  _NotesPageState createState() =&gt; _NotesPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_NotesPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">NotesPage</span>&gt; </span>{
  Future&lt;<span class="hljs-keyword">void</span>&gt; _signOut() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> success = <span class="hljs-keyword">await</span> Services.of(context).authService.signOut();
    <span class="hljs-keyword">if</span> (success) {
      Navigator.pushReplacement(
          context, MaterialPageRoute(builder: (_) =&gt; HomePage()));
    } <span class="hljs-keyword">else</span> {
      ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(<span class="hljs-string">'There was an issue logging out.'</span>)));
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _addNote() <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// TODO</span>
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'Supanotes'</span>),
        actions: [_logOutButton(context)],
      ),
      body: ListView(
        children: [
          FutureBuilder&lt;<span class="hljs-built_in">List</span>&lt;Note&gt;&gt;(
            future: Services.of(context).notesService.getNotes(),
            builder: (context, snapshot) {
              <span class="hljs-keyword">final</span> notes = (snapshot.data ?? [])
                ..sort((x, y) =&gt;
                    y.modifyTime.difference(x.modifyTime).inMilliseconds);
              <span class="hljs-keyword">return</span> Column(
                children: notes.map(_toNoteWidget).toList(),
              );
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        label: Text(<span class="hljs-string">'Add note'</span>),
        icon: Icon(Icons.add),
        onPressed: _addNote,
      ),
    );
  }

  Widget _logOutButton(BuildContext context) {
    <span class="hljs-keyword">return</span> IconButton(
      onPressed: _signOut,
      icon: Icon(Icons.logout),
    );
  }

  Widget _toNoteWidget(Note note) {
    <span class="hljs-keyword">return</span> ListTile(
      title: Text(note.title),
      subtitle: Text(note.content ?? <span class="hljs-string">''</span>),
    );
  }
}
</code></pre>
<p>We use a <code>FutureBuilder</code> to load the notes from the <code>NotesService</code>, and display the notes in a column. The notes are sorted by modify time (latest one first). We also changed the sign out button to be an action in the <code>AppBar</code> instead of a button in the page.</p>
<p>This was previously a stateless widget, but has been converted to a stateful widget for convenience; we will use <code>setState</code> after creating or changing notes to refresh the page.</p>
<p>If your notes page looks like in the screenshot below, you did everything correctly! Next, we're going to implement the logic to add a new note, which will be done through the FAB button that currently doesn't do anything.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621869273743/m-HbF_rf0.png" alt="notes.png" /></p>
<h3 id="heading-creating-notes">Creating notes</h3>
<p>Now, let's create the <code>NotePage</code>, where we'll be able to create a new note.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> NotePage();

  <span class="hljs-meta">@override</span>
  _NotePageState createState() =&gt; _NotePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_NotePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">NotePage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _titleController = TextEditingController();
  <span class="hljs-keyword">final</span> _contentController = TextEditingController();

  Future&lt;<span class="hljs-keyword">void</span>&gt; _saveNote_() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_titleController.text.isEmpty) {
      _showSnackBar(<span class="hljs-string">'Title cannot be empty.'</span>);
    }
    <span class="hljs-keyword">final</span> note = <span class="hljs-keyword">await</span> Services.of(context)
        .notesService
        .createNote(_titleController.text, _contentController.text);
    <span class="hljs-keyword">if</span> (note != <span class="hljs-keyword">null</span>) {
      Navigator.pop(context, note);
    } <span class="hljs-keyword">else</span> {
      _showSnackBar(<span class="hljs-string">'Something went wrong.'</span>);
    }
  }

  <span class="hljs-keyword">void</span> _showSnackBar(<span class="hljs-built_in">String</span> text) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'New note'</span>),
      ),
      body: Column(
        children: &lt;Widget&gt;[
          Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
            child: TextField(
              controller: _titleController,
              decoration: InputDecoration(hintText: <span class="hljs-string">'Title'</span>),
            ),
          ),
          Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
            child: TextField(
              controller: _contentController,
              decoration: InputDecoration(hintText: <span class="hljs-string">'Content'</span>),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _saveNote,
        icon: Icon(Icons.save),
        label: Text(<span class="hljs-string">'Save'</span>),
      ),
    );
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _titleController.dispose();
    _contentController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p>This page widget is simple. Two text fields, one for the title and one for the content. Pressing the FAB button saves the note to the database. When saving, we check that the title is not empty, and show a simple snackbar otherwise.</p>
<p>One thing to note is that since creating the note might take a while, we don't want the users to accidentally tab the "Add note" button twice and create duplicate notes. So we should introduce some mechanism to disable the button until we get the result from the notes service, but that's outside of the scope of this tutorial.</p>
<p>Here is the <code>createNote</code> function in <code>NotesService</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesService</span>
Future&lt;Note?&gt; createNote(<span class="hljs-built_in">String</span> title, <span class="hljs-built_in">String?</span> content) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client
      .from(notes)
      .insert({<span class="hljs-string">'title'</span>: title, <span class="hljs-string">'content'</span>: content}).execute();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">final</span> results = response.data <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">return</span> toNote(results[<span class="hljs-number">0</span>]);
  }
  log(<span class="hljs-string">'Error creating note: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
</code></pre>
<p>Since most values are generated by default, with the exception of the title and content, all we need to pass to our insert query is these two fields. The response contains a list of all inserted records, which in our case is just the one note, which we return in this function, if there were no issues.</p>
<p>Now let's go back to the <code>NotesPage</code> and implement the <code>_addNote_</code> function.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesPage</span>
Future&lt;<span class="hljs-keyword">void</span>&gt; _addNote() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> note = <span class="hljs-keyword">await</span> Navigator.push&lt;Note?&gt;(
    context,
    MaterialPageRoute(builder: (context) =&gt; NotePage()),
  );
  <span class="hljs-keyword">if</span> (note != <span class="hljs-keyword">null</span>) {
    setState(() {});
  }
}
</code></pre>
<p>If a note is returned from the <code>NotePage</code>, we use <code>setState</code> to force the widget to reload, fetching all notes from the database again. We could optimize this part by maybe saving the notes in the widget's state, and rather than loading all notes from the database again, simply appending the new note. But let's keep it this way for simplicity!</p>
<h3 id="heading-editing-notes">Editing notes</h3>
<p>In order to edit notes, let's change the <code>NotePage</code> a bit to support both creating and editing notes. We can pass an optional note to this widget, and if present, we can display its contents in the text fields, and call a function to edit the note rather than create a new one. Here's the updated constructor for <code>NotePage</code> widget:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> Note? note;

  <span class="hljs-keyword">const</span> NotePage({<span class="hljs-keyword">this</span>.note});

  <span class="hljs-meta">@override</span>
  _NotePageState createState() =&gt; _NotePageState();
}
</code></pre>
<p>In the state widget, we override the <code>initState</code> method to populate the text fields with the note title and content if a note was passed in the constructor. The <code>_saveNote</code> function is updated to either create a new note, or update it. We also set the <code>AppBar</code> title accordingly.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// _NotePageState</span>
<span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Scaffold(
    appBar: AppBar(
      title: Text(widget.note != <span class="hljs-keyword">null</span> ? <span class="hljs-string">'Edit note'</span> : <span class="hljs-string">'New note'</span>),
    ),
    ...
  );
}

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  <span class="hljs-keyword">if</span> (widget.note != <span class="hljs-keyword">null</span>) {
    _titleController.text = widget.note!.title;
    _contentController.text = widget.note!.content ?? <span class="hljs-string">''</span>;
  }
}

Future&lt;<span class="hljs-keyword">void</span>&gt; _saveNote() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> title = _titleController.text;
  <span class="hljs-keyword">final</span> content = _contentController.text;
  <span class="hljs-keyword">if</span> (title.isEmpty) {
    _showSnackBar(<span class="hljs-string">'Title cannot be empty.'</span>);
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">final</span> note = <span class="hljs-keyword">await</span> _createOrUpdateNote(title, content);
  <span class="hljs-keyword">if</span> (note != <span class="hljs-keyword">null</span>) {
    Navigator.pop(context, note);
  } <span class="hljs-keyword">else</span> {
    _showSnackBar(<span class="hljs-string">'Something went wrong.'</span>);
  }
}

Future&lt;Note?&gt; _createOrUpdateNote(<span class="hljs-built_in">String</span> title, <span class="hljs-built_in">String</span> content) {
  <span class="hljs-keyword">final</span> notesService = Services.of(context).notesService;
  <span class="hljs-keyword">if</span> (widget.note != <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">return</span> notesService.updateNote(widget.note!.id, title, content);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> notesService.createNote(title, content);
  }
}
</code></pre>
<p>And here is the service function to update the note:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesService</span>
Future&lt;Note?&gt; updateNote(<span class="hljs-built_in">int</span> id, <span class="hljs-built_in">String</span> title, <span class="hljs-built_in">String?</span> content) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client
      .from(notes)
      .update({<span class="hljs-string">'title'</span>: title, <span class="hljs-string">'content'</span>: content, <span class="hljs-string">'modify_time'</span>: <span class="hljs-string">'now()'</span>})
      .eq(<span class="hljs-string">'id'</span>, id)
      .execute();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">final</span> results = response.data <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt;;
    <span class="hljs-keyword">return</span> toNote(results[<span class="hljs-number">0</span>]);
  }
  log(<span class="hljs-string">'Error editing note: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
</code></pre>
<p>The above function updates the title, content, and modify time fields for the note with the above ID. Because of the policy we set up before, the update will only happen if the note with the given ID was created by the current user. Passing <code>now()</code> in the modify time field will set the current time.</p>
<p>Now, let's make it so that if you tap on any note, the edit page will show up:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesPage</span>
Future&lt;<span class="hljs-keyword">void</span>&gt; _editNote(Note note) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> updatedNote = <span class="hljs-keyword">await</span> Navigator.push&lt;Note?&gt;(
    context,
    MaterialPageRoute(builder: (context) =&gt; NotePage(note: note)),
  );
  <span class="hljs-keyword">if</span> (updatedNote != <span class="hljs-keyword">null</span>) {
    setState(() {});
  }
}

Widget _toNoteWidget(Note note) {
  <span class="hljs-keyword">return</span> ListTile(
    title: Text(note.title),
    subtitle: Text(note.content ?? <span class="hljs-string">''</span>),
    onTap: () =&gt; _editNote(note),
  );
}
</code></pre>
<p>Very similarly to the creating notes implementation, if the <code>NotePage</code> is popped with a note, it means the note was updated, so we can rebuild the widget to display the changes.</p>
<p>When tapping on a note, you should now see this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621869242503/MBXR0ODdu.png" alt="edit_note.png" /></p>
<h3 id="heading-deleting-notes">Deleting notes</h3>
<p>So far, we can create new notes, and edit them. The next, and final step for this tutorial, is to delete them!</p>
<p>We'll wrap the note widgets in a <code>Dismissible</code> and call the delete function in the <code>confirmDismiss</code> function. If deletion is successful, we call <code>setState</code> on dismissal to rebuild the widget.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesPage</span>
Widget _toNoteWidget(Note note) {
  <span class="hljs-keyword">return</span> Dismissible(
    key: ValueKey(note.id),
    direction: DismissDirection.endToStart,
    confirmDismiss: (_) =&gt;
        Services.of(context).notesService.deleteNote(note.id),
    onDismissed: (_) =&gt; setState(() {}),
    background: Container(
      padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
      color: Theme.of(context).errorColor,
      alignment: Alignment.centerRight,
      child: Icon(Icons.delete),
    ),
    child: ListTile(
      title: Text(note.title),
      subtitle: Text(note.content ?? <span class="hljs-string">''</span>),
      onTap: () =&gt; _editNote(note),
    ),
  );
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// NotesService</span>
Future&lt;<span class="hljs-built_in">bool</span>&gt; deleteNote(<span class="hljs-built_in">int</span> id) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.from(notes).delete().eq(<span class="hljs-string">'id'</span>, id).execute();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Error deleting note: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>And that's it! </p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>In this part of the tutorial, we showed how to create a table in Supabase, and how to create, read, update and delete records from our Flutter app using the Supabase client. We also discussed policies, a powerful feature by Postgres that makes sure data from the database is secured and cannot be accessed by the wrong users through the Supabase client.</p>
<p>Next up, we'll dive into Supabase's <strong>Storage</strong> offering. We'll make use of this feature by implementing the functionality to attach files to notes. To do this, we will also introduce a simple one-to-many relationship between the notes table and a new attachments table.</p>
<p>If you'd like to be notified when the next part of this series is published, and for any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item><item><title><![CDATA[Going full-stack with Flutter and Supabase - Part 1: Authentication]]></title><description><![CDATA[Introduction
In this tutorial series, we will build a simple notes app, powered by Supabase. Supabase is a product similar to Firebase, which provides services such as authentication and a database, as well as a client which allows you to authenticat...]]></description><link>https://dartling.dev/full-stack-with-flutter-and-supabase-pt-1-authentication</link><guid isPermaLink="true">https://dartling.dev/full-stack-with-flutter-and-supabase-pt-1-authentication</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Christos]]></dc:creator><pubDate>Thu, 13 May 2021 16:37:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1620923979424/OsHgDx4By.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial series, we will build a simple notes app, powered by Supabase. Supabase is a product similar to Firebase, which provides services such as authentication and a database, as well as a client which allows you to authenticate and query the database directly from your Flutter app. By combining Flutter with Supabase, you could build production-ready* apps without having to write or maintain a back-end server.</p>
<p>This series will consist of three parts, each one going over a specific service by Supabase. First, we'll start with <strong>Authentication</strong>, which will allow users to sign in the app. Then, we'll move on to the <strong>Database</strong>, where we'll create a simple schema for our notes app, and allow users to create and view their notes. Last, we'll check out Storage, which we'll make use of by allowing users to attach files to their notes.</p>
<p>This tutorial assumes you already have some experience with Flutter (if not, go <a target="_blank" href="https://flutter.dev/docs/get-started/install">here</a> to get started!). Code snippets will be shared as we go along, but you can also check out the full source code at the repository linked at the end of this post.</p>
<p>* Note that Supabase is still in public beta phase. However, it's still an exciting service, and now is a good time to experiment with it!</p>
<h3 id="heading-what-is-supabase">What is Supabase?</h3>
<p>Supabase calls itself an open source alternative to Firebase. It offers several things: a relational database which you can access directly from the client, built-in user authentication so you can manage users and permissions, as well as storage so you can store large files. Functions are also in the works, which will allow you to run your own code without needing a server.</p>
<h3 id="heading-why-not-firebase">Why not Firebase?</h3>
<p>While Supabase is considered an alternative, it's also very different. As always, there is no one right choice, and the best service to use depends on your use case. But Supabase has two stand-out features that Firebase does not have, which a lot of developers might prefer.</p>
<ul>
<li>Relational database: Supabase is built on top of <a target="_blank" href="https://www.postgresql.org/">PostgreSQL</a>, a powerful relational database that has been around for over 30 years. Firebase's Firestore and Realtime Database offerings are non-relational, so the choice between Supabase and Firebase might be as easy as relational vs non-relational, though there's more to it than that. Firebase is not open source, and has not been around as long as Postgres.</li>
<li>It's all open source: Supabase is built on top of other popular open-source packages, such as <a target="_blank" href="https://postgrest.org/">PostgREST</a> for accessing your database directly from the client, and <a target="_blank" href="https://github.com/netlify/gotrue">GoTrue</a> for user authentication. All that, along with a nice admin interface so you can manage everything. If you wanted, you could host and manage these services on your own. If PostgREST does not fit your use case, you could host a server that will communicate with your Postgres database directly. And if you ever decide that Supabase is not for you, you could always migrate all your data somewhere else. At the end of the day, it's all in a Postgres database.</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<h3 id="heading-setting-up-supabase">Setting up Supabase</h3>
<p>Getting started with Supabase is simple, so we won't go into much detail about its setup. Just head to the <a target="_blank" href="https://supabase.io/">Supabase website</a> and create a project.</p>
<p>You can spend some time to get familiar with the <a target="_blank" href="https://app.supabase.io/">admin interface</a>, but we won't be needing it a lot for this first part. All we will need is your Supabase project's URL and API key, which you can find your project's API settings.</p>
<p>In auth settings, you can also have some options to change your site URL, which is included in emails sent by Supabase when a user signs up (which you can disable), as well as customize the email templates sent for password resets, magic links(!), and invitations.</p>
<p>You can also set up and run Supabase locally for local development by following the guide <a target="_blank" href="https://supabase.io/docs/guides/local-development">here</a>.</p>
<h3 id="heading-creating-a-new-flutter-app">Creating a new Flutter app</h3>
<p>Now that we've got Supabase all set up, it's time to start building the app. Let's create a new Flutter app. Since this is a notes app built on Supabase, I'll be calling it Supanotes!</p>
<pre><code class="lang-sh">flutter create supanotes
</code></pre>
<p>Note: if null safety was not already enabled by default, you can do this by running the command below.</p>
<pre><code class="lang-sh"><span class="hljs-built_in">cd</span> supanotes
dart migrate --apply-changes
</code></pre>
<p>We'll be making use of Dart's null safety features, but it for whatever reason you don't want to enable it for your app, you should still be able to follow along.</p>
<h2 id="heading-part-1-authentication">Part 1: Authentication</h2>
<p>Now, let's make a very simple login page. Here is the UI code so far:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SupanotesApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> supabaseGreen = Color.fromRGBO(<span class="hljs-number">101</span>, <span class="hljs-number">217</span>, <span class="hljs-number">165</span>, <span class="hljs-number">1.0</span>);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Supanotes'</span>,
      theme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: toMaterialColor(supabaseGreen),
      ),
      home: HomePage(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> HomePage();

  <span class="hljs-meta">@override</span>
  _HomePageState createState() =&gt; _HomePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomePage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _emailController = TextEditingController();
  <span class="hljs-keyword">final</span> _passwordController = TextEditingController();

  <span class="hljs-keyword">void</span> _signUp() {
    <span class="hljs-comment">// Sign up logic will go here</span>
  }

  <span class="hljs-keyword">void</span> _signIn() {
    <span class="hljs-comment">// Sign in logic will go here</span>
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).primaryColor,
        title: Text(<span class="hljs-string">'Supanotes'</span>),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Padding(
              padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
              child: TextField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: InputDecoration(hintText: <span class="hljs-string">'Email'</span>),
              ),
            ),
            Padding(
              padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
              child: TextField(
                controller: _passwordController_,
                obscureText: <span class="hljs-keyword">true</span>,
                decoration: InputDecoration(hintText: <span class="hljs-string">'Password'</span>),
              ),
            ),
            ElevatedButton.icon(
              onPressed: _signIn,
              icon: Icon(Icons.login),
              label: Text(<span class="hljs-string">'Sign in'</span>),
            ),
            ElevatedButton.icon(
              onPressed: _signUp,
              icon: Icon(Icons.app_registration),
              label: Text(<span class="hljs-string">'Sign up'</span>),
            ),
          ],
        ),
      ),
    );
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p>The <code>toMaterialColor</code> function is taken from <a target="_blank" href="https://blog.usejournal.com/creating-a-custom-color-swatch-in-flutter-554bcdcb27f3">this post here</a> (thanks Filip!).</p>
<h3 id="heading-adding-the-supabase-package">Adding the Supabase package</h3>
<p>Let's implement the logic for sign in and sign up. First, we add the <code>supabase</code> package to our app.</p>
<pre><code class="lang-sh">flutter pub add supabase
</code></pre>
<p>Next, we'll create an <code>AuthService</code> that will handle anything auth related. In order to pass this service to any widget that needs it, we'll create an <code>InheritedWidget</code> that will hold all services we might need.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Services</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">InheritedWidget</span> </span>{
  <span class="hljs-keyword">final</span> AuthService authService;

  Services._({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.authService,
    <span class="hljs-keyword">required</span> Widget child,
  }) : <span class="hljs-keyword">super</span>(child: child);

  <span class="hljs-keyword">factory</span> Services({<span class="hljs-keyword">required</span> Widget child}) {
    <span class="hljs-keyword">final</span> client = SupabaseClient(supabaseUrl, supabaseUrl);
    <span class="hljs-keyword">final</span> authService = AuthService(client.auth);
    <span class="hljs-keyword">return</span> Services._(authService: authService, child: child);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> updateShouldNotify(InheritedWidget oldWidget) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
  }

  <span class="hljs-keyword">static</span> Services of(BuildContext context) {
    <span class="hljs-keyword">return</span> context.dependOnInheritedWidgetOfExactType&lt;Services&gt;()!;
  }
}
</code></pre>
<p>In this <code>InheritedWidget</code>, we initialize the Supabase client, and create the <code>AuthService</code>. To initialise the <code>SupabaseClient</code>, simply pass the URL and API keys from the API settings on the Supabase admin interface.</p>
<h3 id="heading-the-gotrue-client">The GoTrue client</h3>
<p>Supabase uses GoTrue for authentication. <code>client.auth</code> returns the GoTrue client, which is all the <code>AuthService</code> will need.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span> </span>{
  <span class="hljs-keyword">final</span> GoTrueClient _client;

  AuthService(<span class="hljs-keyword">this</span>._client);

  Future&lt;<span class="hljs-built_in">bool</span>&gt; signUp(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) {
    <span class="hljs-comment">// sign up logic goes here</span>
  }

  Future&lt;<span class="hljs-built_in">bool</span>&gt; signIn(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) {
    <span class="hljs-comment">// sign in logic goes here</span>
  }

  Future&lt;<span class="hljs-built_in">bool</span>&gt; signOut() {
    <span class="hljs-comment">// sign out logic goes here</span>
  }
}
</code></pre>
<p>All three functions will return <code>true</code> if the response was successful, otherwise false.</p>
<p>Let's start with the <code>signUp</code> function:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// AuthService</span>
Future&lt;<span class="hljs-built_in">bool</span>&gt; signUp(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signUp(email, password);
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    log(<span class="hljs-string">'Sign up was successful for user ID: <span class="hljs-subst">${response.user!.id}</span>'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Sign up error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>This function will accept the email and password which we will pass from the text field widgets, and create a user. The client will return a <code>GotrueSessionResponse</code>, which will contain an error if something goes wrong, or a <code>User</code> and <code>Session</code> object.</p>
<p>We don't currently need to do anything with the <code>User</code> and <code>Session</code> objects, though it contains useful information we could make use of, such as the user's ID, last sign in date, and email confirmation date. However, these will always be accessible through <code>GoTrueClient#currentUser</code> and <code>GoTrueClient#currentSession</code> so we can grab them from the client when we need them.</p>
<p>Below are also the <code>signIn</code> and <code>signOut</code> functions, which are very similar to <code>signUp</code>.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// AuthService</span>
Future&lt;<span class="hljs-built_in">bool</span>&gt; signIn(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signIn(email: email, password: password);
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    log(<span class="hljs-string">'Sign in was successful for user ID: <span class="hljs-subst">${response.user!.id}</span>'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Sign in error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}

Future&lt;<span class="hljs-built_in">bool</span>&gt; signOut() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signOut();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Log out error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>Now, let's call these <code>AuthService</code> functions in <code>HomePage</code>. Since the service is part of the <code>Services</code> <code>InheritedWidget</code>, we can freely access it through <code>Services.of(context).authService</code>. But to do that, we should first wrap <code>MaterialApp</code> with the <code>Services</code> widget. Our top level app widget should look like this:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SupanotesApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> supabaseGreen = Color.fromRGBO(<span class="hljs-number">101</span>, <span class="hljs-number">217</span>, <span class="hljs-number">165</span>, <span class="hljs-number">1.0</span>);

  <span class="hljs-keyword">const</span> SupanotesApp();

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Services(
      child: MaterialApp(
        title: <span class="hljs-string">'Supanotes'</span>,
        theme: ThemeData(
          brightness: Brightness.dark,
          primarySwatch: toMaterialColor(supabaseGreen),
        ),
        home: HomePage(),
      ),
    );
  }
}
</code></pre>
<p>Now, we can call <code>AuthService</code> functions from any widget. Let's start with the logic for sign up.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// HomePage</span>
<span class="hljs-keyword">void</span> _signUp() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> success = <span class="hljs-keyword">await</span> Services.of(context)
      .authService
      .signUp(_emailController.text, _passwordController.text);
  <span class="hljs-keyword">if</span> (success) {
    <span class="hljs-keyword">await</span> Navigator.pushReplacement(
        context, MaterialPageRoute(builder: (_) =&gt; NotesPage()));
  } <span class="hljs-keyword">else</span> {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(<span class="hljs-string">'Something went wrong.'</span>)));
  }
}
</code></pre>
<p>If the response was successful, we navigate to a new page, the <code>NotesPage</code>, where we will display the user's notes. We call <code>pushReplacement</code> rather than <code>push</code>, since we don't want the user pressing back and ending up in the home page by mistake.</p>
<p>If the response fails, we just show a generic snack bar. This could display more useful information, based on the actual error.</p>
<p>The function for signing in is pretty much the same. We can reuse some of the code for both functions, and our code now looks like this:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// HomePage</span>
<span class="hljs-keyword">void</span> _signUp() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> success = <span class="hljs-keyword">await</span> Services.of(context)
      .authService
      .signUp(_emailController.text, _passwordController.text);
  <span class="hljs-keyword">await</span> _handleResponse(success);
}

<span class="hljs-keyword">void</span> _signIn() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> success = <span class="hljs-keyword">await</span> Services.of(context)
      .authService
      .signIn(_emailController.text, _passwordController.text);
  <span class="hljs-keyword">await</span> _handleResponse(success);
}

Future&lt;<span class="hljs-keyword">void</span>&gt; _handleResponse(<span class="hljs-built_in">bool</span> success) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">if</span> (success) {
    <span class="hljs-keyword">await</span> Navigator.pushReplacement(
        context, MaterialPageRoute(builder: (_) =&gt; NotesPage()));
  } <span class="hljs-keyword">else</span> {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(<span class="hljs-string">'Something went wrong.'</span>)));
  }
}
</code></pre>
<p>Now let's build the <code>NotesPage</code>. For now, it'll just be an empty page, with a button to sign out. Here's the full code:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotesPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> NotesPage();

  Future&lt;<span class="hljs-keyword">void</span>&gt; _signOut(BuildContext context) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> success = <span class="hljs-keyword">await</span> Services.of(context).authService.signOut();
    <span class="hljs-keyword">if</span> (success) {
      Navigator.pushReplacement(
          context, MaterialPageRoute(builder: (_) =&gt; HomePage()));
    } <span class="hljs-keyword">else</span> {
      ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(<span class="hljs-string">'There was an issue logging out.'</span>)));
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'Supanotes'</span>),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
              child: Text(<span class="hljs-string">'Your notes will show up here.'</span>),
            ),
            ElevatedButton.icon(
              onPressed: () <span class="hljs-keyword">async</span> {
                <span class="hljs-keyword">await</span> _signOut(context);
              },
              icon: Icon(Icons.login),
              label: Text(<span class="hljs-string">'Sign out'</span>),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>And that's it! We now have a proper authentication system set up for our Supanotes app. Now any requests to the Supabase client will include the signed in user info as well, so we can continue with the next part: creating and listing notes.</p>
<h3 id="heading-gotrue-and-json-web-tokens">GoTrue and JSON Web Tokens</h3>
<p>GoTrue uses JSON Web Tokens (JWT) for authentication. On a successful sign in/sign up, the response contains an access token, and a refresh token. The access token is included in every request to Supabase. When decoded, this token contains the user's unique ID, and this is how Supabase knows which user is performing the request. Access tokens are short-lived, and expire by default every one hour (you can configure this through the admin interface, but only do this if you have a good reason). You can read more about JWT <a target="_blank" href="https://jwt.io/">here</a>.</p>
<p>The access token is only valid for a short time, and we definitely don't want the user having to sign in every one hour. This is what the refresh token is for. The refresh token is valid for longer than an hour, and can be used to request new tokens. Let's take a look at some of the source code of the <code>GoTrueClient</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _saveSession(Session session) {
  currentSession = session;
  currentUser = session.user;
  <span class="hljs-keyword">final</span> tokenExpirySeconds = session.expiresIn;

  <span class="hljs-keyword">if</span> (autoRefreshToken &amp;&amp; tokenExpirySeconds != <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">if</span> (_refreshTokenTimer != <span class="hljs-keyword">null</span>) _refreshTokenTimer!.cancel();

    <span class="hljs-keyword">final</span> timerDuration = <span class="hljs-built_in">Duration</span>(seconds: tokenExpirySeconds - <span class="hljs-number">60</span>);
    _refreshTokenTimer = Timer(timerDuration, () {
      _callRefreshToken();
    });
  }
}
</code></pre>
<p>If <code>autoRefreshToken</code> is set to <code>true</code>, a timer is set to automatically "refresh" the tokens by using the refresh token, one minute before the access token expires. <code>autoRefreshToken</code> is <code>true</code> by default, and can be changed through an optional parameter in the <code>GoTrueClient</code> constructor.</p>
<p>Now, what happens if the user exits the app? We lose access to both tokens, and have no way of requesting new tokens either, so the user would have to sign in again.</p>
<h3 id="heading-keeping-users-signed-in">Keeping users signed in</h3>
<p>To keep users signed in even after re-opening the app, we need to somehow request new tokens. <code>GoTrueClient</code> has a <code>recoverSession</code> function, which uses the refresh token mentioned before to request new tokens.</p>
<p>In order to recover the session, we need to persist the session data of a successful sign in. In this example, we'll use the <code>shared_preferences</code> package, but you might also want to use something like <code>flutter_secure_storage</code> if there are security concerns. Data stored in shared preferences is only accessible by the app which stored them, but this is not the case for rooted devices.</p>
<pre><code class="lang-sh">flutter pub add shared_preferences
</code></pre>
<p>Conveniently, the <code>Session</code> object in the <code>GoTrueResponse</code> contains a function to get the JSON string to persist. So all we have to do is store this string in <code>SharedPreferences</code>, and fetch it when we want to recover the session. Here's the updated <code>signUp</code> and <code>signIn</code> functions:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// AuthService</span>
<span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> supabaseSessionKey = <span class="hljs-string">'supabase_session'</span>;

Future&lt;<span class="hljs-built_in">bool</span>&gt; signUp(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signUp(email, password);
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    log(<span class="hljs-string">'Sign up was successful for user ID: <span class="hljs-subst">${response.user!.id}</span>'</span>);
    _persistSession(response.data!);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Sign up error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}

Future&lt;<span class="hljs-built_in">bool</span>&gt; signIn(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signIn(email: email, password: password);
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    log(<span class="hljs-string">'Sign in was successful for user ID: <span class="hljs-subst">${response.user!.id}</span>'</span>);
    _persistSession(response.data!);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Sign in error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}

Future&lt;<span class="hljs-keyword">void</span>&gt; _persistSession(Session session) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
  log(<span class="hljs-string">'Persisting session string'</span>);
  <span class="hljs-keyword">await</span> prefs.setString(supabaseSessionKey, session.persistSessionString);
}
</code></pre>
<p>Note that it might make sense to initialize <code>SharedPreferences</code> once and pass it to any services that might need it, but let's keep it like this for simplicity.</p>
<p>Okay, so we have all we need to recover the session after the user exists the app. What now? In order to decide which page widget to show (<code>HomePage</code> if user is not signed in, <code>NotesPage</code> otherwise), we can check if there is a persisted session string, and attempt to recover the session with <code>GoTrueClient#recoverSession</code>. If there's no session string stored, or if recovering the session fails, then we can show the <code>HomePage</code>. If recovering the session works, then the user is successfully signed in! We can go straight to the <code>NotesPage</code>.</p>
<p>Let's introduce a new function in <code>AuthService</code>, that will attempt to recover the session.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// AuthService</span>
Future&lt;<span class="hljs-built_in">bool</span>&gt; recoverSession() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
  <span class="hljs-keyword">if</span> (prefs.containsKey(supabaseSessionKey)) {
    log(<span class="hljs-string">'Found persisted session string, attempting to recover session'</span>);
    <span class="hljs-keyword">final</span> jsonStr = prefs.getString(supabaseSessionKey)!;
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.recoverSession(jsonStr);
    <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
      log(<span class="hljs-string">'Session successfully recovered for user ID: <span class="hljs-subst">${response.user!.id}</span>'</span>);
      _persistSession(response.data!);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }
  }
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>Next, let's call this function to decide which page to show first. Here's the updated <code>build</code> function of the root <code>SupanotesApp</code> widget:</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Services(
    child: MaterialApp(
      title: <span class="hljs-string">'Supanotes'</span>,
      theme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: toMaterialColor(supabaseGreen),
      ),
      home: Builder(
        builder: (context) {
          <span class="hljs-keyword">return</span> FutureBuilder&lt;<span class="hljs-built_in">bool</span>&gt;(
            future: Services.of(context).authService.recoverSession(),
            builder: (context, snapshot) {
              <span class="hljs-keyword">final</span> sessionRecovered = snapshot.data ?? <span class="hljs-keyword">false</span>;
              <span class="hljs-keyword">return</span> sessionRecovered ? NotesPage() : HomePage();
            },
          );
        },
      ),
    ),
  );
}
</code></pre>
<p>Note that we are wrapping the <code>FutureBuilder</code> with a <code>Builder</code>, as the <code>Services</code> object will not be available in the context of <code>SupanotesApp</code>.</p>
<p>And that's it! If you now try to perform a hot restart, or exit and re-open the app, you'll see that you'll still be signed in! If the refresh token has expired, the <code>recoverSession</code> function will attempt to refresh the tokens, otherwise it will resume with the persisted session.</p>
<p>The <code>GoTrueClient</code> is not a big class, so I'd recommend taking a better look at the source code if you're interested in seeing how it all works behind the scenes.</p>
<p>There is one more thing we need to do. When a user signs out, we should clear this persisted session string. Otherwise, if a user signs out and then re-opens the app, they could be signed back in!</p>
<p>Here's the updated <code>signOut</code> function:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-built_in">bool</span>&gt; signOut() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _client.signOut();
  <span class="hljs-keyword">if</span> (response.error == <span class="hljs-keyword">null</span>) {
    log(<span class="hljs-string">'Successfully logged out; clearing session string'</span>);
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    prefs.remove(supabaseSessionKey);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
  log(<span class="hljs-string">'Log out error: <span class="hljs-subst">${response.error!.message}</span>'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
</code></pre>
<h3 id="heading-more-auth-features">More auth features</h3>
<p>Supabase authentication also supports third party logins, but we won't show an example of how to use this functionality in this post. These OAuth providers are supported:</p>
<ul>
<li>Google</li>
<li>GitHub</li>
<li>GitLab</li>
<li>Azure</li>
<li>Facebook</li>
<li>BitBucket</li>
<li>Apple</li>
<li>Twitter</li>
</ul>
<p>There is also support for magic links! If you call the <code>GoTrueClient#signIn</code> function without a password, this will send the user an email with a one-time login link. Both third party logins and magic links could be good potential blog posts, so watch this space!</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>That's all for the first part of this tutorial series on Supabase with Flutter, focusing on <strong>Authentication</strong>. You can find the full code on <a target="_blank" href="https://github.com/dartling/supanotes/tree/pt1-auth">GitHub</a>. I hope you found it helpful, and would love it if you could share some feedback in the comments! In the next part, we will take a look at Supabase's <strong>Database</strong> offering.</p>
<p>If you'd like to be notified when the next part of this series is published, and for any future tutorials, please sign up with your email below.</p>
]]></content:encoded></item></channel></rss>