<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Kevine Nzapdi - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Kevine Nzapdi - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:24:00 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/gunkev/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Multilingual Social Recipe Application with Flutter and Strapi ]]>
                </title>
                <description>
                    <![CDATA[ Hey there! In this project, you will build a multilingual social recipe application using Flutter and Strapi. Flutter is an open-source UI software development kit created by Google. It allows you to build beautiful and highly interactive user interf... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-multilingual-social-recipe-app-with-flutter-and-strapi/</link>
                <guid isPermaLink="false">67f59a4c27d15057ec14c438</guid>
                
                    <category>
                        <![CDATA[ Recipe Apps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Strapi ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ multilingual ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kevine Nzapdi ]]>
                </dc:creator>
                <pubDate>Tue, 08 Apr 2025 21:51:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743509325302/fd7d5d6c-9a48-4037-9cc2-3b35a92b6006.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hey there!</p>
<p>In this project, you will build a multilingual social recipe application using Flutter and Strapi.</p>
<p>Flutter is an open-source UI software development kit created by Google. It allows you to build beautiful and highly interactive user interfaces for mobile, web, and desktop from a single codebase.</p>
<p>Strapi, on the other hand, is a headless CMS that makes it easy to create, manage and distribute content anywhere you need – all from one place.</p>
<p>The multilingual feature of the application will allow users from different parts of the world to interact with the app in their native language, making it more user-friendly and accessible. This feature is particularly beneficial for a social recipe application where users share recipes from different cuisines and cultures.</p>
<p>In this application, users will be able to view recipes, request a specific recipe, share their favorite recipes, and like or comment on recipes.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-demo">Demo</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-models">Create Models</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-languages-and-enable-internationalization-in-strapi">Add Languages and Enable Internationalization in Strapi</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-content">Add Recipe Content</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-add-recipe-english-content">Add Recipe English Content</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-french-content">Add Recipe French Content</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-japanese-content">Add Recipe Japanese Content</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-generate-api-token-and-set-permissions">Generate API Token and Set permissions</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-set-user-roles-and-permissions">Set User Roles and Permissions</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-flutter">Set up Flutter</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-install-packages">Install Packages</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-add-assets">Add Assets</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-taking-a-look-at-maindart">Taking a look at main.dart</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-add-environment-variables">Add Environment Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-models-1">Create Models</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-reciperequest">1. RecipeRequest</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-step">2. Step</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-description">3. Description</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-textcontent">4. TextContent</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-comment">5. Comment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-recipe">6. Recipe</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-create-services">Create Services</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-class-variables">1. Class Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-helper-methods">2. Helper Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-user-operations">3. User Operations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-data-fetching-and-manipulation">4. Data Fetching and Manipulation</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-authorization-and-authentication">Authorization and Authentication</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-registration">Registration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-login">Login</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-build-app-components">Build App Components</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-drawer">Drawer</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-appbar">AppBar</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-fetch-recipes">Fetch Recipes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-view-recipe">View Recipe</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-request-recipe-screen">Create Request Recipe Screen</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-user-profile-screen">Create User Profile Screen</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-the-app">Test the App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, make sure you have:</p>
<ul>
<li><p><a target="_blank" href="https://nodejs.org/en">Node.js</a> installed.</p>
</li>
<li><p>Basic knowledge of <a target="_blank" href="https://flutter.dev/">Flutter</a></p>
</li>
<li><p>Basic understanding of Strapi with this <a target="_blank" href="https://docs.strapi.io/dev-docs/quick-start">quick guide</a></p>
</li>
</ul>
<h2 id="heading-demo">Demo</h2>
<p>Here’s what you will be building in the tutorial:</p>
<ol>
<li><p>Authentication and Authorization: <a target="_blank" href="https://drive.google.com/file/d/1cjnnRD38wQsj_sYHl5EG5uM3AyHJUWdf/view?usp=sharing">Demo</a></p>
</li>
<li><p>Comment and Likes: <a target="_blank" href="https://drive.google.com/file/d/1wM0xQ2R7inL90gAkiYjLcGV5df4AmzH1/view?usp=sharing">Demo</a></p>
</li>
<li><p>Request recipe: <a target="_blank" href="https://drive.google.com/file/d/1xlxSFD2qU2rOE4kICiX-py_JxvgrphqK/view?usp=sharing">Demo</a></p>
</li>
<li><p>Language Switch: <a target="_blank" href="https://drive.google.com/file/d/14lmBCIgX4VIKOFmS9pG71cIHH7HLaW1J/view?usp=sharing">Demo</a></p>
</li>
</ol>
<p>You can get the full code of the application from <a target="_blank" href="https://github.com/Gunkev/flutter_strapi_multilingual_app">this GitHub repository</a>.</p>
<h2 id="heading-create-models">Create Models</h2>
<p>Once you have set up a Strapi project with <a target="_blank" href="https://docs.strapi.io/dev-docs/installation/cli">this quick guide</a>, create two models, Recipe and RecipeRequest, in the Strapi admin panel.</p>
<p>A recipe typically has the following elements:</p>
<ul>
<li><p>Title: <code>text</code> which represents the title of the recipe</p>
</li>
<li><p>Ingredients: <code>text</code> which represent the of ingredients of the recipe</p>
</li>
<li><p>Likes: <code>int</code> which represent the number of likes</p>
</li>
<li><p>Author: <code>relation</code> which represent the author of the recipe</p>
</li>
<li><p>Comments: <code>relation</code> which represent the list of comments of a specific recipe</p>
</li>
<li><p>Steps: <code>rich text</code> which represents the main content of the recipe</p>
</li>
<li><p>Description: <code>rich text</code> which represents a description of what the recipe is like</p>
</li>
<li><p>Comment Count: <code>int</code> which represents the number of comment a recipe has</p>
</li>
<li><p>Cover Image: <code>media</code> which represents the cover image of the recipe</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743504946186/e1be7d98-fff8-4e2e-b446-1ddbf541d1c0.png" alt="recipe model" class="image--center mx-auto" width="1360" height="834" loading="lazy"></p>
<p>Make sure to enable internationalization for Recipe Content Type when you create it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743504992503/73842540-4b8d-4412-9c51-1c55e095e83e.png" alt="enable internationalization" class="image--center mx-auto" width="827" height="311" loading="lazy"></p>
<p>A recipe request typically has:</p>
<ul>
<li><p>Title, which is <code>text</code> that represents the title of the request</p>
</li>
<li><p>Description, which is <code>rich text</code> that represents the content of the request</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505019316/6d172672-af58-4a6d-b0a3-cb713ee32dd2.png" alt="recipe request model" class="image--center mx-auto" width="1383" height="496" loading="lazy"></p>
<p>A comment typical has:</p>
<ul>
<li><p>Author, which is a <code>relation</code> that represents the author of the comment</p>
</li>
<li><p>Content, which is <code>text</code> that represents the content of the comments</p>
</li>
<li><p>Date, which is a <code>date</code> that represents the published date of the comment</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505036935/92d02ecb-9a86-43f9-99a9-a2a534aab871.png" alt="comment model" class="image--center mx-auto" width="1367" height="435" loading="lazy"></p>
<p>The user will also have 4 new fields:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505060587/cda0be86-298b-4053-b8ae-8c894e07a592.png" alt="additional user fields" class="image--center mx-auto" width="1346" height="199" loading="lazy"></p>
<h2 id="heading-add-languages-and-enable-internationalization-in-strapi">Add Languages and Enable Internationalization in Strapi</h2>
<p>The application will support three different languages (English, French, and Japanese). English is the default language, so you need to add the two others. In the Strapi panel, you’ll need to navigate to Settings and then Internationalization and add French and Japanese. I will explain the process in detail in the next sections.</p>
<h2 id="heading-add-recipe-content">Add Recipe Content</h2>
<p>Next, you will populate some recipe data in English, French, and Japanese.</p>
<h3 id="heading-add-recipe-english-content">Add Recipe English Content</h3>
<p>Since English is the default language, go to Content manager, then select Recipe, and then select <strong>Create new entry</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505111608/3fb2d615-d649-4c22-8a73-87cbcbd38bdb.png" alt="list of added recipes" class="image--center mx-auto" width="1368" height="441" loading="lazy"></p>
<h3 id="heading-add-recipe-french-content">Add Recipe French Content</h3>
<p>For French, navigate to Settings, select Internationalization, and then under global settings click on <strong>Add new locale.</strong> Here you will add the French language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505140738/a8e5b0d0-0871-46b1-8fb0-2921c84b913a.png" alt="french language config" class="image--center mx-auto" width="840" height="403" loading="lazy"></p>
<p>Back to the Content manager, click on recipe and select the French language in the top right corner. Then choose the <strong>Create recipe entry</strong> in French.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505164770/2ad75e5a-a20d-496d-9fe3-75fdc3cf64b1.png" alt="french model version" class="image--center mx-auto" width="1356" height="420" loading="lazy"></p>
<h3 id="heading-add-recipe-japanese-content">Add Recipe Japanese Content</h3>
<p>Navigate back to Settings and Internationalization, and under global settings again click on <strong>Add new locale.</strong> Now you will add the Japanese language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505187987/91251e4e-4172-4ce5-9e53-78ca12352af4.png" alt="japanese language config" class="image--center mx-auto" width="833" height="400" loading="lazy"></p>
<p>Back to the Content manager, click on recipe and select the Japanese language in the top right corner. Then select <strong>Create new entry</strong> in Japanese.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505218903/0e7b7025-8473-4012-ab54-130fe5b63164.png" alt="Japenese recipe list" class="image--center mx-auto" width="1367" height="446" loading="lazy"></p>
<h2 id="heading-generate-api-token-and-set-permissions">Generate API Token and Set permissions</h2>
<p>Once you’ve added the content for the various languages, it’s time to create your API and set the necessary permissions.</p>
<p>To do this, navigate to Settings, then API Tokens, and then Create API Token. Add the details of your key there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505239235/5a183f54-6469-4d4e-aa62-d81f4dccf8ae.png" alt="API token creation" class="image--center mx-auto" width="1433" height="830" loading="lazy"></p>
<ul>
<li><p>Token duration: choose Unlimited</p>
</li>
<li><p>Token Type: Custom. The custom type allows you to specify permission for certain entities.</p>
</li>
</ul>
<p>Next, still in the Create API Token screen, scroll down to the permission section and set the permission to “Select all” for Comments, and RecipeRequest, upload, email, content type, i18n, and User permissions like in the screenshot below for Recipe-request:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505260256/84f6f009-4c7a-4136-8497-6c22b9fa87de.png" alt="enable permission for recipe request" class="image--center mx-auto" width="690" height="332" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744116611459/f5518d2e-5200-40b3-9b74-ed0b0adeeabb.png" alt="f5518d2e-5200-40b3-9b74-ed0b0adeeabb" class="image--center mx-auto" width="474" height="660" loading="lazy"></p>
<p>Then click on the Save button in the top right corner to generate your API key. Copy and save the key in your PC as you won’t be able to see it again</p>
<h3 id="heading-set-user-roles-and-permissions">Set User Roles and Permissions</h3>
<p>You’ll also need to set the user roles and permissions using the <a target="_blank" href="https://docs.strapi.io/dev-docs/plugins/users-permissions">User and Permission Plugin</a>. It allows you to manage what both authenticated and non-authenticated users can do in your application.</p>
<p>Head to the Settings section of the dashboard and go to Roles under the User and Permission plugin.</p>
<p>We have two types of users:</p>
<ul>
<li><p>Authenticated users</p>
</li>
<li><p>Public users</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744117848867/8023d7c4-c07b-43dc-ba00-89a958bc0672.png" alt="8023d7c4-c07b-43dc-ba00-89a958bc0672" class="image--center mx-auto" width="1890" height="762" loading="lazy"></p>
<p>Select the authenticated users and give them the following permissions for:</p>
<p>Comment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505301527/3939448a-48f4-44fc-baa9-a528a78e73c7.png" alt="enable permission for comments" class="image--center mx-auto" width="628" height="666" loading="lazy"></p>
<p>Recipe:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505327113/f9224713-105d-4cdb-9a5b-4846d1789b07.png" alt="enable authorized user to perdorm action on recipe model" class="image--center mx-auto" width="615" height="735" loading="lazy"></p>
<p>Request-recipe:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505346092/d328c629-4ea9-40a0-baa6-90a96ae364ec.png" alt="enable permission for recipe request model" class="image--center mx-auto" width="616" height="734" loading="lazy"></p>
<p>Also select all for Content-type builder, i18n, and Upload and then save.</p>
<p>Public users can only read recipes and comments:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505362706/4d776b8f-84f9-4a41-a1d4-73b1a2fd6a4c.png" alt="limit comment operation for public users" class="image--center mx-auto" width="688" height="347" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505369235/54ed5f73-9841-43bf-a088-0079358b6b05.png" alt="limit recipe operations for public user" class="image--center mx-auto" width="684" height="349" loading="lazy"></p>
<h2 id="heading-set-up-flutter">Set Up Flutter</h2>
<p>Once you have <a target="_blank" href="https://docs.flutter.dev/get-started/install/windows/desktop">set up Flutte</a><a target="_blank" href="https://docs.flutter.dev/get-started/install/windows/desktop">r</a> in your environment, run the following command to bootstrap a new application in your favorite directory:</p>
<pre><code class="lang-bash">flutter create flutter_recipe_app
</code></pre>
<p>To see your app in action, you need to run it on a mobile device. You can either:</p>
<ul>
<li><p>Use an <strong>emulator</strong> (a virtual Android or iOS device that runs on your computer), or</p>
</li>
<li><p>Connect a <strong>physical device</strong> (like your smartphone) to your computer with a USB cable.</p>
</li>
</ul>
<p>Once your emulator or device is ready, navigate into the newly created project folder:</p>
<pre><code class="lang-bash">flutter run
</code></pre>
<p>This command builds the app and starts it on your connected device or emulator.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505498936/6e1e461d-9fee-4e19-81e0-65d25ddebd63.png" alt="flutter starter app" class="image--center mx-auto" width="1080" height="2220" loading="lazy"></p>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Now let's look at the file structure of the project:</p>
<pre><code class="lang-bash">flutter_recipe_app/
|
|-- .dart_tool/
|-- .idea/
|-- android/ [flutter_recipe_app_android]
|   |-- assets/
|   |   |-- images/
|   |   |-- translations/
|
|-- build/
|-- ios/
|-- lib/
|   |-- components/
|   |   |-- appBar.dart
|   |   |-- drawer.dart
|   |
|   |-- models/
|   |   |-- recipe.dart
|   |
|   |-- screens/
|   |   |-- detail.dart
|   |   |-- home.dart
|   |   |-- login.dart
|   |   |-- profile.dart
|   |   |-- requestRecipe.dart
|   |   |-- signUp.dart
|   |
|   |-- utils/
|       |-- server2.dart
|
|-- main.dart
|-- <span class="hljs-built_in">test</span>/
|-- .env
</code></pre>
<p>The structure is organized as follows:</p>
<ul>
<li><p><code>.dart_tool/</code>: Contains Dart tools and build outputs.</p>
</li>
<li><p><code>.idea/</code>: IDE-specific settings.</p>
</li>
<li><p><code>android/</code>: Android-specific project files, including custom assets like images and translations.</p>
</li>
<li><p><code>build/</code>: Generated files from the build process.</p>
</li>
<li><p><code>ios/</code>: iOS-specific project files.</p>
</li>
<li><p><code>lib/</code>: The main source directory for Dart code, which includes:</p>
<ul>
<li><p><code>components/</code>: Reusable widgets or UI components like <code>appBar</code> and <code>drawer</code>.</p>
</li>
<li><p><code>models/</code>: Data models for your application, like <code>recipe</code>.</p>
</li>
<li><p><code>screens/</code>: Individual screens of the app, such as the <code>recipe details</code>, <code>home</code>, <code>login</code>, <code>profile</code>, <code>request recipe</code> and <code>signUp</code> screens of the app</p>
</li>
<li><p><code>utils/</code>: Utilities and helper functions, like <code>server2.dart</code> for the server communication logic.</p>
</li>
</ul>
</li>
<li><p><code>main.dart</code>: The entry point of the Flutter application.</p>
</li>
<li><p><code>test/</code>: Directory for test files.</p>
</li>
<li><p><code>.env</code>: Environment-specific variables file.</p>
</li>
</ul>
<p>This setup is typical for a moderately complex Flutter application, segregating functionality into manageable, logical sections for better organization and maintainability.</p>
<h2 id="heading-install-packages">Install Packages</h2>
<p>In this tutorial, we’re using five main packages:</p>
<ul>
<li><p><a target="_blank" href="https://pub.dev/packages/flutter_dotenv">flutter_dotenv</a>: to manage environment variables</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/http">http</a>: to handle HTTP requests and interact with <a target="_blank" href="https://docs.strapi.io/dev-docs/api/rest">Strapi REST API</a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/shared_preferences">shared_preferences</a>: persists key-value data on the device like user login tokens</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/provider">provider</a>: for state management and updating your UI reactively when the underlying state changes</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/easy_localization">easy_localization</a>: for managing translations and locale data. It supports both JSON and YAML file formats for defining translations.</p>
</li>
</ul>
<p>In your <code>pubspec.yaml</code> file, add the following lines:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-string">...</span>
  <span class="hljs-attr">flutter_dotenv:</span> <span class="hljs-string">^5.1.0</span>
  <span class="hljs-attr">http:</span> <span class="hljs-string">^1.1.0</span>
  <span class="hljs-attr">shared_preferences:</span> <span class="hljs-string">^2.2.2</span>
  <span class="hljs-attr">provider:</span> <span class="hljs-string">^6.1.2</span>
  <span class="hljs-attr">easy_localization:</span> <span class="hljs-string">^3.0.7</span>
</code></pre>
<p>Then run the command below to install the packages:</p>
<pre><code class="lang-bash">flutter pub get
</code></pre>
<h3 id="heading-add-assets">Add Assets</h3>
<p>Add the path to your assets in your <code>pubspec.yaml</code> file found at the root of your project:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter:</span>
  <span class="hljs-attr">uses-material-design:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">assets:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/translations/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/images/</span>
</code></pre>
<p>The translations folder contains the list of your translations while the images folder hosts the photos of your application.</p>
<h3 id="heading-taking-a-look-at-maindart">Taking a look at main.dart</h3>
<p>In the <code>main.dart</code> file, you need to set up your localization, load environment variables, and a list of providers for dependency injection:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:flutter_recipe_app/screens/home.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/login.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/requestRecipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/signUp.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/utils/server.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;

Future&lt;<span class="hljs-keyword">void</span>&gt; main() <span class="hljs-keyword">async</span>{
  <span class="hljs-comment">// Ensure all bindings are initialized</span>
  WidgetsFlutterBinding.ensureInitialized();
  <span class="hljs-keyword">await</span> EasyLocalization.ensureInitialized();

  <span class="hljs-comment">// Load environment variables</span>
  <span class="hljs-keyword">await</span> dotenv.load(fileName: <span class="hljs-string">".env"</span>);
  runApp(EasyLocalization(
    supportedLocales: <span class="hljs-keyword">const</span> [
      Locale(<span class="hljs-string">'en'</span>),
      Locale(<span class="hljs-string">'fr'</span>, <span class="hljs-string">'FR'</span>),
      Locale(<span class="hljs-string">'ja'</span>, <span class="hljs-string">'JP'</span>)],
    path: <span class="hljs-string">'assets/translations'</span>, <span class="hljs-comment">//</span>
    fallbackLocale: Locale(<span class="hljs-string">'en'</span>),
    child: MyApp(),
  ));
}

<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-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MultiProvider(
      providers: [
        Provider(create: (_) =&gt; ApiService()),
      ],
      child: MaterialApp(
        title: tr(<span class="hljs-string">'app_description'</span>),
        localizationsDelegates: context.localizationDelegates,
        supportedLocales: context.supportedLocales,
        locale: context.locale,
        initialRoute: <span class="hljs-string">'/home'</span>,
        routes: {
          <span class="hljs-string">'/request'</span>: (context) =&gt; RecipeRequestScreen(),
          <span class="hljs-string">'/login'</span>: (context) =&gt; LoginScreen(),
          <span class="hljs-string">'/register'</span>: (context) =&gt; RegisterScreen(),
          <span class="hljs-string">'/home'</span>: (context) =&gt; HomeScreen(), <span class="hljs-comment">// Implement HomeScreen</span>
        },
      ),
    );
  }
}
</code></pre>
<p>From the code snippet above, the <code>WidgetsFlutterBinding.ensureInitialized()</code> ensures that all Flutter bindings are initialized before any other operations and the <code>EasyLocalization.ensureInitialized()</code> initializes the EasyLocalization package to handle translations.</p>
<p>Load the environment variables with <code>dotenv.load(fileName: ".env")</code> to read variables from the <code>.env</code> file. The <code>runApp</code> function wraps the <code>MyApp</code> widget with the <code>EasyLocalization</code> widget, which is configured to support English (<code>en</code>), French (<code>fr_FR</code>), and Japanese (<code>ja_JP</code>) locales. The path for translation files is set to <code>'assets/translations'</code>, and the fallback locale is set to English.</p>
<p>It also creates the main routes of the recipe application and sets <code>home</code> as the initial route.</p>
<h2 id="heading-add-environment-variables">Add Environment Variables</h2>
<p>You will store configuration data such as API keys, environment-specific URLs (base URL, recipe endpoints, comments endpoints), and other sensitive or configurable data outside your codebase using the <code>flutter_dotenv</code> package you installed earlier. Create an <code>.env</code> file in your root directory and add your environment variables:</p>
<pre><code class="lang-bash">BASE_URL=your-base-url
USERS_ENDPOINT=/auth/<span class="hljs-built_in">local</span>
USERS_ENDPOINT_REG=/auth/<span class="hljs-built_in">local</span>/register
ACCESS_TOKEN=your-api-key
RECIPE_ENDPOINT=/recipes
COMMENT_ENDPOINT=/comments
R_REQUEST_ENDPOINT=/recipe-requests
</code></pre>
<ul>
<li><p><code>BASE_URL</code>: This is the base URL for your Strapi backend server. The <code>/api</code> means that all API endpoints are accessed via this base path. This URL is used to construct full URLs for all API requests by appending specific endpoints to it.</p>
</li>
<li><p><code>USERS_ENDPOINT</code>: This endpoint typically handles login operations where existing users authenticate by submitting their credentials.</p>
</li>
<li><p><code>USERS_ENDPOINT_REG</code>: This is the registration endpoint for new users.</p>
</li>
<li><p><code>ACCESS_TOKEN</code>: This is the API token you created earlier which is used for authenticating API requests.</p>
</li>
<li><p><code>RECIPE_ENDPOINT</code>: This endpoint is used to fetch a list of recipes or a single recipe. You can also use it to post new recipes, or update or delete a recipe.</p>
</li>
<li><p><code>COMMENT_ENDPOINT</code>: This endpoint manages comments related to recipes.</p>
</li>
<li><p><code>R_REQUEST_ENDPOINT</code>: This endpoint manages requests related to recipes.</p>
</li>
</ul>
<h2 id="heading-create-models-1">Create Models</h2>
<p>Here you will create the different models of the app. You can create all the models in a single file or create them in individual files. In this tutorial, we’ll create all the models in a single file which is <code>lib/models/recipe.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;

<span class="hljs-comment">// models recipe_ request</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeRequest</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">List</span>&lt;Description&gt; description

  RecipeRequest({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <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>.description,
  });

  <span class="hljs-keyword">factory</span> RecipeRequest.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) {
    <span class="hljs-keyword">var</span> attr = json[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-keyword">var</span> attributes = json[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-built_in">List</span>&lt;Description&gt; descriptionList = (attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [])
        .map((desc) =&gt; Description.fromJson(desc)).toList();

    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Parsed Recipe: <span class="hljs-subst">${json[<span class="hljs-string">'id'</span>]}</span> - Descriptions: <span class="hljs-subst">${descriptionList.length}</span>"</span>);

    <span class="hljs-keyword">return</span> RecipeRequest(
      id: json[<span class="hljs-string">'id'</span>] ?? <span class="hljs-number">0</span>,
      title: attr[<span class="hljs-string">'title'</span>] ?? <span class="hljs-string">'No title'</span>,
      description: descriptionList,
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'title'</span>: title,
      <span class="hljs-string">'description'</span>: description.map((desc) =&gt; desc.toJson()).toList(),
      <span class="hljs-comment">// 'id': id</span>
    };
  }
}

<span class="hljs-comment">// step model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Step</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;TextContent&gt; children;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int?</span> level;

  Step({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.children, <span class="hljs-keyword">this</span>.level});

  <span class="hljs-keyword">factory</span> Step.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) {
    <span class="hljs-keyword">var</span> childrenList = json[<span class="hljs-string">'children'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [];
    <span class="hljs-built_in">List</span>&lt;TextContent&gt; parsedChildren = childrenList.map((child) =&gt; TextContent.fromJson(child)).toList();
    <span class="hljs-keyword">return</span> Step(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      children: parsedChildren,
      level: json[<span class="hljs-string">'level'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'children'</span>: children.map((child) =&gt; child.toJson()).toList(),
      <span class="hljs-string">'level'</span>: level,
    };
  }
}

<span class="hljs-comment">// description model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Description</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;TextContent&gt; children;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int?</span> level;

  Description({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.children, <span class="hljs-keyword">this</span>.level});

  <span class="hljs-keyword">factory</span> Description.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) {
    <span class="hljs-keyword">var</span> childrenList = json[<span class="hljs-string">'children'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [];
    <span class="hljs-built_in">List</span>&lt;TextContent&gt; parsedChildren = childrenList.map((child) =&gt; TextContent.fromJson(child)).toList();
    <span class="hljs-keyword">return</span> Description(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      children: parsedChildren,
      level: json[<span class="hljs-string">'level'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'children'</span>: children.map((child) =&gt; child.toJson()).toList(),
      <span class="hljs-string">'level'</span>: level,
    };
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TextContent</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> text;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool?</span> bold;

  TextContent({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.text, <span class="hljs-keyword">this</span>.bold});

  <span class="hljs-keyword">factory</span> TextContent.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) {
    <span class="hljs-keyword">return</span> TextContent(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      text: json[<span class="hljs-string">'text'</span>] ?? <span class="hljs-string">''</span>,
      bold: json[<span class="hljs-string">'bold'</span>] ?? <span class="hljs-keyword">false</span>,
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'text'</span>: text,
      <span class="hljs-string">'bold'</span>: bold,
    };
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comment</span> </span>{
  <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">String</span> author;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> createdAt;

  Comment({
    <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>.author,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.createdAt,
  });

  <span class="hljs-keyword">factory</span> Comment.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) {
    <span class="hljs-keyword">var</span> attributes = json[<span class="hljs-string">'attributes'</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">var</span> authorData = attributes[<span class="hljs-string">'comment_author'</span>]?[<span class="hljs-string">'data'</span>]?[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-keyword">return</span> Comment(
      content: attributes[<span class="hljs-string">'content'</span>] ?? <span class="hljs-string">'No content'</span>,
      author: authorData[<span class="hljs-string">'username'</span>] ?? <span class="hljs-string">'Unknown'</span>,
      createdAt: <span class="hljs-built_in">DateTime</span>.parse(attributes[<span class="hljs-string">'createdAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toString()),
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'content'</span>: content,
      <span class="hljs-string">'author'</span>: author,
      <span class="hljs-string">'createdAt'</span>: createdAt.toIso8601String(),
    };
  }
}

<span class="hljs-comment">//recipe model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Recipe</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">List</span>&lt;Description&gt; description;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> ingredients;
  <span class="hljs-keyword">late</span> <span class="hljs-built_in">int</span> likes;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> createdAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> updatedAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> publishedAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Step&gt; steps;
  <span class="hljs-keyword">late</span> <span class="hljs-built_in">int</span> commentCount;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Comment&gt; comments;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> coverImageUrl;

  Recipe({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <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>.description,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.ingredients,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.likes,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.createdAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.updatedAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.publishedAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.steps,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.commentCount,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.comments,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.coverImageUrl
  });

  <span class="hljs-keyword">factory</span> Recipe.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) {
    <span class="hljs-keyword">var</span> attr = json[<span class="hljs-string">'attributes'</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-comment">// Parse descriptions</span>
    <span class="hljs-built_in">List</span>&lt;Description&gt; descriptionList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'description'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      descriptionList = (attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((desc) =&gt; Description.fromJson(desc)).toList();
    }

    <span class="hljs-comment">// Parse steps</span>
    <span class="hljs-built_in">List</span>&lt;Step&gt; stepsList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'steps'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'steps'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      stepsList = (attr[<span class="hljs-string">'steps'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((step) =&gt; Step.fromJson(step)).toList();
    }

    <span class="hljs-comment">// Parse comments</span>
    <span class="hljs-built_in">List</span>&lt;Comment&gt; commentList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'comments'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      commentList = (attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((comment) =&gt; Comment.fromJson(comment)).toList();
    }

    <span class="hljs-comment">// var attr = json['attributes'] as Map&lt;String, dynamic&gt;;</span>
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> baseUrl = dotenv.env[<span class="hljs-string">'BASE_URL'</span>]!;

    <span class="hljs-comment">// Ensure image URL is correctly prefixed</span>
    <span class="hljs-built_in">String</span> coverImageUrl = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'cover'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'cover'</span>][<span class="hljs-string">'data'</span>] != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">var</span> imageUrl = attr[<span class="hljs-string">'cover'</span>][<span class="hljs-string">'data'</span>][<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'url'</span>];
      coverImageUrl = imageUrl.startsWith(<span class="hljs-string">'http'</span>)
          ? imageUrl
          : baseUrl + imageUrl; 
    }

    <span class="hljs-keyword">return</span> Recipe(
        id: json[<span class="hljs-string">'id'</span>] ?? <span class="hljs-number">0</span>,
        title: attr[<span class="hljs-string">'title'</span>] ?? <span class="hljs-string">'No title'</span>,
        description: descriptionList,
        ingredients: attr[<span class="hljs-string">'ingredients'</span>] ?? <span class="hljs-string">'No ingredients'</span>,
        likes: attr[<span class="hljs-string">'likes'</span>] ?? <span class="hljs-number">0</span>,
        createdAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'createdAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        updatedAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'updatedAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        publishedAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'publishedAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        steps: stepsList,
        commentCount: commentList.length,
        comments: commentList,
        coverImageUrl: coverImageUrl
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'id'</span>: id,
      <span class="hljs-string">'title'</span>: title,
      <span class="hljs-string">'description'</span>: description.map((desc) =&gt; desc.toJson()).toList(),
      <span class="hljs-string">'ingredients'</span>: ingredients,
      <span class="hljs-string">'likes'</span>: likes,
      <span class="hljs-string">'createdAt'</span>: createdAt.toIso8601String(),
      <span class="hljs-string">'updatedAt'</span>: updatedAt.toIso8601String(),
      <span class="hljs-string">'publishedAt'</span>: publishedAt.toIso8601String(),
      <span class="hljs-string">'steps'</span>: steps.map((step) =&gt; step.toJson()).toList(),
      <span class="hljs-string">'commentCount'</span>: commentCount,
      <span class="hljs-string">'comments'</span>: comments.map((comment) =&gt; comment.toJson()).toList(),
      <span class="hljs-string">'cover'</span>: coverImageUrl
    };
  }
}
</code></pre>
<p>Let’s go over this code piece by piece, as it’s a lot:</p>
<h3 id="heading-1-reciperequest">1. <strong>RecipeRequest</strong></h3>
<p>The <code>RecipeRequest</code> class represents the class that allows a user to request a recipe. It has three properties (<code>id</code>, <code>title</code>, and a list of <code>Description</code> objects as defined in the Strapi backend) with 2 methods:</p>
<ul>
<li><p><code>fromJson</code>: to convert JSON data into a <code>RecipeRequest</code> object, including parsing a list of descriptions.</p>
</li>
<li><p><code>toJson</code>: to convert a <code>RecipeRequest</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-2-step">2. <strong>Step</strong></h3>
<p>Represents the cooking steps in a recipe. It contains a list of <code>Textcontent</code> objects, and each Step object has a type, level, and children as it is a richtext type. It also has two methods:</p>
<ul>
<li><p><code>fromJson</code>: to parse JSON to create a <code>Step</code> object.</p>
</li>
<li><p><code>toJson</code>: to convert a <code>Step</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-3-description">3. <strong>Description</strong></h3>
<p>This class also contains a list of <code>TextContent</code> objects (<code>children</code>). Each <code>Description</code> object also has a <code>type</code> and an optional <code>level</code> to indicate hierarchical structure. It has two methods, too:</p>
<ul>
<li><p><code>fromJson</code>: to convert JSON into a <code>Description</code> object.</p>
</li>
<li><p><code>toJson</code>: to serialise a <code>Description</code> object to JSON.</p>
</li>
</ul>
<h3 id="heading-4-textcontent">4. <strong>TextContent</strong></h3>
<p>This class is designed to represent individual pieces of text within larger structures. Each <code>TextContent</code> object can contain a string of text (<code>text</code>), the type of text (<code>type</code>), and an optional boolean to indicate whether the text is bold (<code>bold</code>)</p>
<ul>
<li><p><code>fromJson</code>: Parses JSON into a <code>TextContent</code> object.</p>
</li>
<li><p><code>toJson</code>: Converts a <code>TextContent</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-5-comment">5. <strong>Comment</strong></h3>
<p>As the name indicates, this represents a comment written by a use. It has three properties: the comment <code>content</code>, <code>author</code>, and <code>createdAt</code>. Like others, it also includes two methods:</p>
<ul>
<li><p><code>fromJson</code>: to extract and construct a <code>Comment</code> object from JSON, including parsing author data.</p>
</li>
<li><p><code>toJson</code>: to serializes a <code>Comment</code> object to JSON.</p>
</li>
</ul>
<h3 id="heading-6-recipe">6. <strong>Recipe</strong></h3>
<p>Finally, there is the <code>Recipe</code> class which is the main recipe object. It contains various details about a recipe, including id, title, descriptions, ingredients, likes, timestamps, steps, comment count, comment list, and a cover image URL. We have the:</p>
<ul>
<li><p><code>fromJson</code>: to build a <code>Recipe</code> object from JSON data. This includes parsing lists of descriptions, steps, and comments. It also adjusts the image URL to ensure it is absolute.</p>
</li>
<li><p><code>toJson</code>: to convert the <code>Recipe</code> object to JSON format.</p>
</li>
</ul>
<p>As you can see, each class is designed to handle specific parts of the recipe data, with <code>fromJson</code> methods to parse JSON into Dart objects and <code>toJson</code> methods to serialize Dart objects back to JSON.</p>
<h2 id="heading-create-services">Create Services</h2>
<p>Now that your environment variables are set up, you can create different services for communicating with the server. In your <code>lib/utils/server.dart</code> file, add the code below:</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">'dart:developer'</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:flutter_dotenv/flutter_dotenv.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-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiService</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> baseUrl = dotenv.env[<span class="hljs-string">'BASE_URL'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> registerEndpoint = dotenv.env[<span class="hljs-string">'USERS_ENDPOINT_REG'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> loginEndpoint = dotenv.env[<span class="hljs-string">'USERS_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> accessToken = dotenv.env[<span class="hljs-string">'ACCESS_TOKEN'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> recipeEndpoint = dotenv.env[<span class="hljs-string">'RECIPE_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> commentEndpoint = dotenv.env[<span class="hljs-string">'COMMENT_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> requestEndpoint = dotenv.env[<span class="hljs-string">'R_REQUEST_ENDPOINT'</span>]!;

  <span class="hljs-comment">// Helper method to get headers with optional JWT token</span>
  Future&lt;<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt;&gt; _getHeaders({<span class="hljs-built_in">bool</span> includeJwt = <span class="hljs-keyword">false</span>}) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> headers = {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">"Bearer <span class="hljs-subst">$accessToken</span>"</span>,
    };
    <span class="hljs-keyword">if</span> (includeJwt) {
      <span class="hljs-keyword">final</span> jwt = <span class="hljs-keyword">await</span> getJwt();
      <span class="hljs-keyword">if</span> (jwt != <span class="hljs-keyword">null</span>) {
        headers[<span class="hljs-string">"Authorization"</span>] = <span class="hljs-string">"Bearer <span class="hljs-subst">$jwt</span>"</span>;
      }
    }
    <span class="hljs-keyword">return</span> headers;
  }

  <span class="hljs-comment">// Get JWT</span>
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getJwt() <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">return</span> prefs.getString(<span class="hljs-string">'jwt'</span>);
  }

  <span class="hljs-comment">// Set JWT</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; setJwt(<span class="hljs-built_in">String</span> jwt) <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">await</span> prefs.setString(<span class="hljs-string">'jwt'</span>, jwt);
  }

  <span class="hljs-comment">// Remove JWT</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; removeJwt() <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">await</span> prefs.remove(<span class="hljs-string">'jwt'</span>);
  }

  <span class="hljs-comment">// Set User Data</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; setUserData(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; data) <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">await</span> prefs.setString(<span class="hljs-string">'userId'</span>, data[<span class="hljs-string">'user'</span>][<span class="hljs-string">'id'</span>].toString());
    <span class="hljs-keyword">await</span> prefs.setString(<span class="hljs-string">'username'</span>, data[<span class="hljs-string">'user'</span>][<span class="hljs-string">'username'</span>]);
  }

  <span class="hljs-comment">// Remove User Data</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; removeUserData() <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">await</span> prefs.remove(<span class="hljs-string">'userId'</span>);
    <span class="hljs-keyword">await</span> prefs.remove(<span class="hljs-string">'username'</span>);
  }

  <span class="hljs-comment">// User Registration</span>
  Future&lt;http.Response&gt; register(<span class="hljs-built_in">String</span> username, <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> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$registerEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"username"</span>: username,
          <span class="hljs-string">"email"</span>: email,
          <span class="hljs-string">"password"</span>: password,
        }),
      );
      <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error registering user: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// User Login</span>
  Future&lt;http.Response&gt; login(<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> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$loginEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"identifier"</span>: email,
          <span class="hljs-string">"password"</span>: password,
        }),
      );

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">final</span> data = json.decode(response.body);
        <span class="hljs-keyword">await</span> setJwt(data[<span class="hljs-string">'jwt'</span>]);
        <span class="hljs-keyword">await</span> setUserData(data);
      }

      <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error logging in user: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// User Logout</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> removeJwt();
    <span class="hljs-keyword">await</span> removeUserData();
  }

  <span class="hljs-comment">// Fetch Recipes</span>
  Future&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt; fetchRecipes(BuildContext context) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> localeCode = context.locale.toString().replaceAll(<span class="hljs-string">'_'</span>, <span class="hljs-string">'-'</span>);
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> lang = localeCode == <span class="hljs-string">'en'</span> ? <span class="hljs-string">'en'</span> : localeCode;
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>?locale=<span class="hljs-subst">$lang</span>&amp;populate=*'</span>);
    <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">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
      <span class="hljs-keyword">var</span> jsonResponse = jsonDecode(response.body);
      <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; dataList = jsonResponse[<span class="hljs-string">'data'</span>];
      <span class="hljs-built_in">List</span>&lt;Recipe&gt; recipes = [];

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> dataList) {
        <span class="hljs-keyword">try</span> {
          recipes.add(Recipe.fromJson(item));
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Failed to parse item: <span class="hljs-subst">$e</span>'</span>);
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Item data: <span class="hljs-subst">$item</span>'</span>);
        }
      }

      <span class="hljs-keyword">return</span> recipes;
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to load recipes: HTTP <span class="hljs-subst">${response.statusCode}</span>'</span>);
    }
  }

  <span class="hljs-comment">// Fetch Comments</span>
    Future&lt;<span class="hljs-built_in">List</span>&lt;Comment&gt;&gt; fetchComments(<span class="hljs-built_in">int</span> recipeId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$commentEndpoint</span>?filters[recipe][id][\$eq]=<span class="hljs-subst">$recipeId</span>&amp;populate=comment_author'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(url, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response fetch status: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response fetch body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> jsonData = jsonDecode(response.body);
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Parsed JSON: <span class="hljs-subst">$jsonData</span>"</span>);

        <span class="hljs-keyword">if</span> (jsonData != <span class="hljs-keyword">null</span> &amp;&amp; jsonData.containsKey(<span class="hljs-string">'data'</span>)) {
          <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; data = jsonData[<span class="hljs-string">'data'</span>];
          <span class="hljs-keyword">return</span> data.map&lt;Comment&gt;((json) {
            <span class="hljs-keyword">if</span> (json == <span class="hljs-keyword">null</span> || json[<span class="hljs-string">'attributes'</span>] == <span class="hljs-keyword">null</span>) {
              <span class="hljs-built_in">print</span>(<span class="hljs-string">'json or json[\'attributes\'] is null'</span>);
              <span class="hljs-keyword">return</span> Comment(content: <span class="hljs-string">'Invalid'</span>, author: <span class="hljs-string">'Invalid'</span>, createdAt: <span class="hljs-built_in">DateTime</span>.now());
            }
            <span class="hljs-keyword">return</span> Comment.fromJson(json);
          }).toList();
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Data field is missing or null in the response'</span>);
          <span class="hljs-keyword">return</span> [];
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Failed to load comments with status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
        <span class="hljs-keyword">return</span> [];
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error server fetching comments: <span class="hljs-subst">$e</span>'</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error fetching comments: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  Future&lt;Comment&gt; postComment(<span class="hljs-built_in">String</span> content, <span class="hljs-built_in">int</span> recipeId, <span class="hljs-built_in">String</span> authorId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$commentEndpoint</span>?populate=comment_author'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"data"</span>: {
            <span class="hljs-string">"content"</span>: content,
            <span class="hljs-string">"recipe"</span>: recipeId,
            <span class="hljs-string">"comment_author"</span>: authorId,
          },
        }),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Post comment response status: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Post comment response body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span> || response.statusCode == <span class="hljs-number">201</span>) {
        <span class="hljs-keyword">var</span> jsonData = jsonDecode(response.body);
        <span class="hljs-keyword">return</span> Comment.fromJson(jsonData[<span class="hljs-string">'data'</span>]);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to post comment'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error posting comment: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; updateCommentCount(<span class="hljs-built_in">int</span> recipeId, {<span class="hljs-keyword">required</span> <span class="hljs-built_in">bool</span> increment}) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> recipeUrl = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>/<span class="hljs-subst">$recipeId</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Fetch the current recipe data</span>
      <span class="hljs-keyword">final</span> recipeResponse = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(recipeUrl, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Fetch recipe response status: <span class="hljs-subst">${recipeResponse.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Fetch recipe response body: <span class="hljs-subst">${recipeResponse.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (recipeResponse.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> recipeData = jsonDecode(recipeResponse.body)[<span class="hljs-string">'data'</span>];
        <span class="hljs-built_in">int</span> currentComments = recipeData[<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'comments'</span>] ?? <span class="hljs-number">0</span>;
        <span class="hljs-built_in">int</span> updatedComments = increment ? currentComments + <span class="hljs-number">1</span> : currentComments - <span class="hljs-number">1</span>;

        <span class="hljs-comment">// Ensure updatedComments is not negative</span>
        <span class="hljs-keyword">if</span> (updatedComments &lt; <span class="hljs-number">0</span>) {
          updatedComments = <span class="hljs-number">0</span>;
        }

        <span class="hljs-comment">// Update the recipe with the new comment count</span>
        <span class="hljs-keyword">final</span> updateResponse = <span class="hljs-keyword">await</span> http.put(
          recipeUrl,
          headers: <span class="hljs-keyword">await</span> _getHeaders(),
          body: json.encode({
            <span class="hljs-string">"data"</span>: {
              <span class="hljs-string">"comments"</span>: updatedComments,
            },
          }),
        );

        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Update recipe response status: <span class="hljs-subst">${updateResponse.statusCode}</span>'</span>);
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Update recipe response body: <span class="hljs-subst">${updateResponse.body}</span>'</span>);

        <span class="hljs-keyword">if</span> (updateResponse.statusCode != <span class="hljs-number">200</span>) {
          <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to update comment count'</span>);
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to fetch recipe data'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error updating comment count: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error updating comment count: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  <span class="hljs-comment">// Like Recipe</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; likeRecipe(<span class="hljs-built_in">int</span> recipeId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> recipeUrl = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>/<span class="hljs-subst">$recipeId</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Fetch the current recipe data</span>
      <span class="hljs-keyword">final</span> recipeResponse = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(recipeUrl, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-keyword">if</span> (recipeResponse.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> recipeData = jsonDecode(recipeResponse.body)[<span class="hljs-string">'data'</span>];
        <span class="hljs-built_in">int</span> currentLikes = recipeData[<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'likes'</span>] ?? <span class="hljs-number">0</span>;
        <span class="hljs-built_in">int</span> updatedLikes = currentLikes + <span class="hljs-number">1</span>;

        <span class="hljs-comment">// Update the recipe with the new likes count</span>
        <span class="hljs-keyword">final</span> updateResponse = <span class="hljs-keyword">await</span> http.put(
          recipeUrl,
          headers: <span class="hljs-keyword">await</span> _getHeaders(),
          body: json.encode({
            <span class="hljs-string">"data"</span>: {
              <span class="hljs-string">"likes"</span>: updatedLikes,
            },
          }),
        );

        <span class="hljs-keyword">if</span> (updateResponse.statusCode != <span class="hljs-number">200</span>) {
          <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to update likes count'</span>);
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to fetch recipe data'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error liking recipe: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error liking recipe: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  <span class="hljs-comment">// Submit Recipe Request</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; submitRecipeRequest(RecipeRequest r_request) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$requestEndpoint</span>'</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(includeJwt: <span class="hljs-keyword">true</span>),
        body: jsonEncode({
          <span class="hljs-string">'data'</span>: r_request.toJson(), <span class="hljs-comment">// Wrap the request in a 'data' object</span>
        }),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response body: <span class="hljs-subst">${response.body}</span>'</span>);
      <span class="hljs-keyword">if</span> (response.statusCode != <span class="hljs-number">200</span> &amp;&amp; response.statusCode != <span class="hljs-number">201</span>) {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to submit recipe request'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error submitting recipe request: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// Fetch User Requested Recipes</span>
  Future&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt; fetchUserRequestedRecipes() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$requestEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(includeJwt: <span class="hljs-keyword">true</span>),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> jsonResponse = jsonDecode(response.body);
        <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; data = jsonResponse[<span class="hljs-string">'data'</span>];
        <span class="hljs-keyword">return</span> data.map((json) =&gt; RecipeRequest.fromJson(json)).toList();
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to load user requested recipes'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error fetching user requested recipes: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }
}
</code></pre>
<p>The <code>ApiService</code> class from the code above is a utility for handling various operations related to user authentication and data fetching from a backend server. This service uses HTTP requests to communicate with the Strapi server.</p>
<p>There are four main entities:</p>
<h3 id="heading-1-class-variables">1. Class Variables</h3>
<ul>
<li><p><code>baseUrl</code> is the base URL.</p>
</li>
<li><p><code>registerEndpoint</code>, <code>loginEndpoint</code>, <code>recipeEndpoint</code>, <code>commentEndpoint</code>, <code>requestEndpoint</code> are the specific endpoints for registration, login, recipes, comments, and requests.</p>
</li>
<li><p><code>accessToken</code> is the token used for API authentication.</p>
</li>
</ul>
<h3 id="heading-2-helper-methods">2. Helper Methods</h3>
<ul>
<li><p><code>_getHeaders</code> prepares the headers for HTTP requests and it optionally includes a JWT token if <code>includeJwt</code> is true.</p>
</li>
<li><p><code>getJwt</code> retrieves the JWT token from shared preferences.</p>
</li>
<li><p><code>setJwt</code> and <code>setUserData</code> store the JWT token and user data (ID and username) in shared preferences once the user logs in.</p>
</li>
<li><p><code>removeJwt</code> and <code>removeUserData</code> remove the JWT token and user data from shared preferences, respectively, and log the user out.</p>
</li>
</ul>
<h3 id="heading-3-user-operations">3. User Operations</h3>
<ul>
<li><p><code>register</code> registers a new user with the given username, email, and password. It sends a POST request to the registration endpoint with the user details.</p>
</li>
<li><p><code>login</code> logs in a user with the given email and password. If successful, it stores the received JWT token and user data.</p>
</li>
<li><p><code>logout</code> logs out the user by removing the JWT token and user data from shared preferences.</p>
</li>
</ul>
<h3 id="heading-4-data-fetching-and-manipulation">4. Data Fetching and Manipulation</h3>
<ul>
<li><p><code>fetchRecipes</code> fetches a list of recipes based on the current locale (language) from the backend. It handles parsing the JSON response into a list of <code>Recipe</code> objects.</p>
</li>
<li><p><code>fetchComments</code> fetches comments for a specific recipe by its ID. It populates the <code>comment_author</code> field and returns a list of <code>Comment</code> objects.</p>
</li>
<li><p><code>postComment</code> posts a new comment on a specific recipe. It sends the comment content, recipe ID, and author ID to the backend.</p>
</li>
<li><p><code>updateCommentCount</code> updates the comment count for a specific recipe. It first fetches the current count, modifies it, and then updates it on the backend.</p>
</li>
<li><p><code>likeRecipe</code>: Increments the like count for a specific recipe by fetching the current count, adding one, and updating the backend.</p>
</li>
<li><p><code>submitRecipeRequest</code> submits a new recipe request to the backend. It sends the request data wrapped in a <code>data</code> object.</p>
</li>
<li><p><code>fetchUserRequestedRecipes</code> fetches a list of recipes requested by a specific user from the backend.</p>
</li>
</ul>
<h2 id="heading-authorization-and-authentication">Authorization and Authentication</h2>
<p>Authorization is what allows a user to access a particular resource and determines if a user can perform certain actions within the application like commenting on a recipe, liking a recipe, or requesting a recipe.</p>
<p>On the other hand, authentication is the process of validating and verifying a user.</p>
<p>There are many Authorization and Authentication methods, but in this tutorial we’ll use password-based authentication and an API Key for authorization.</p>
<h3 id="heading-registration">Registration</h3>
<p>In the <code>lib/screen/signUp.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'login.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RegisterScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _RegisterScreenState createState() =&gt; _RegisterScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RegisterScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RegisterScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> TextEditingController usernameController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController emailController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController passwordController = TextEditingController();
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    usernameController.dispose();
    emailController.dispose();
    passwordController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _register() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = <span class="hljs-keyword">true</span>;
      });

      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> Provider.of&lt;ApiService&gt;(context, listen: <span class="hljs-keyword">false</span>)
          .register(usernameController.text, emailController.text, passwordController.text);

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

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-comment">// Navigate to the login screen after successful registration</span>
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (_) =&gt; LoginScreen()),
        );
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Handle error</span>
        showDialog(
          context: context,
          builder: (context) =&gt; AlertDialog(
            title: Text(tr(<span class="hljs-string">'register_fail'</span>)),
            content: Text(tr(<span class="hljs-string">'register_error'</span>)),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text(tr(<span class="hljs-string">'ok'</span>)),
              ),
            ],
          ),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(tr(<span class="hljs-string">'register'</span>))),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: usernameController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'username'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'username_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: emailController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'email'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_required'</span>);
                  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[^@]+@[^@]+\.[^@]+'</span>).hasMatch(value)) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_invalid'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: passwordController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'password'</span>)),
                obscureText: <span class="hljs-keyword">true</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'password_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              _isLoading
                  ? CircularProgressIndicator()
                  : ElevatedButton(
                onPressed: _register,
                child: Text(tr(<span class="hljs-string">'register'</span>)),
              ),
              TextButton(
                onPressed: () {
                  <span class="hljs-comment">// Navigate to the login screen</span>
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(builder: (_) =&gt; LoginScreen()),
                  );
                },
                child: Text(
                  tr(<span class="hljs-string">"have_account"</span>),
                  style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">16</span>),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>This code provides a user-friendly registration interface for the recipe application. The <code>RegisterScreen</code> class is a stateful widget that manages the registration process.</p>
<p>The <code>_register</code> method validates the form and calls the <code>register</code> method from the <code>ApiService</code>. If the registration is successful (indicated by a 200 HTTP status code), it redirects to the login screen. If it fails, an error dialog is displayed with a message.</p>
<p>The code above also employs form validation to ensure that users enter valid information. The username and password fields must not be empty, and the email field must follow a proper email format.</p>
<p>Upon submission, the form displays a loading indicator while the app communicates with the server to register the user.</p>
<p>The form's state is managed using a GlobalKey, and controllers for the text fields are properly disposed of to free up resources when the widget is removed from the tree.</p>
<h3 id="heading-login">Login</h3>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'signUp.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _LoginScreenState createState() =&gt; _LoginScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_LoginScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">LoginScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> TextEditingController emailController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController passwordController = TextEditingController();
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</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();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _login() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = <span class="hljs-keyword">true</span>;
      });

      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> Provider.of&lt;ApiService&gt;(context, listen: <span class="hljs-keyword">false</span>)
          .login(emailController.text, passwordController.text);

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

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        Navigator.pushReplacementNamed(context, <span class="hljs-string">'/home'</span>);
      } <span class="hljs-keyword">else</span> {
        showDialog(
          context: context,
          builder: (context) =&gt; AlertDialog(
            title: Text(tr(<span class="hljs-string">'login_failed'</span>)),
            content: Text(tr(<span class="hljs-string">'invalid_email_password'</span>)),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text(tr(<span class="hljs-string">'ok'</span>)),
              ),
            ],
          ),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(tr(<span class="hljs-string">'login'</span>))),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: emailController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'email'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_required'</span>);
                  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[^@]+@[^@]+\.[^@]+'</span>).hasMatch(value)) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_invalid'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: passwordController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'password'</span>)),
                obscureText: <span class="hljs-keyword">true</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'password_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              _isLoading
                  ? CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _login,
                      child: Text(tr(<span class="hljs-string">'login'</span>)),
                    ),
              TextButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (_) =&gt; RegisterScreen()),
                  );
                },
                child: Text(
                  tr(<span class="hljs-string">"dont_have_account"</span>),
                  style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">16</span>),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>LoginScreen</code> contains two input fields for the user's email and password, and it validates the inputs before attempting to log in. When the user submits the form, the app checks if the input is valid. If valid, it sets a loading indicator and sends a login request to the backend API.</p>
<p>If the login is successful, the app navigates to the home screen, whereas if the login fails, an alert dialog is displayed to inform the user of the invalid email or password. The form also uses a <code>GlobalKey</code> to manage its state and ensures that the text controllers are properly disposed of when the widget is removed from the tree.</p>
<h2 id="heading-build-app-components">Build App Components</h2>
<h3 id="heading-drawer">Drawer</h3>
<p>The Drawer is a side panel that slides in from the left (by default) and provides navigation options for the user. It’s a great way to organize your app’s sections without crowding the main screen.</p>
<p>In our app, the drawer will include links to the Request recipe screen, Profile, Logout, and languages for authenticated users.</p>
<p>In the <code>lib/components/drawer.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../screens/profile.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../screens/requestRecipe.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomDrawer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _CustomDrawerState createState() =&gt; _CustomDrawerState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_CustomDrawerState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">CustomDrawer</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _username;
  <span class="hljs-built_in">String?</span> _userId;

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

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>);
      _username = prefs.getString(<span class="hljs-string">'username'</span>);
      _userId = prefs.getString(<span class="hljs-string">'userId'</span>);
    });
  }

  <span class="hljs-keyword">void</span> _navigateToLogin() {
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <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">await</span> prefs.clear();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _username = <span class="hljs-keyword">null</span>;
      _userId = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-keyword">void</span> _changeLanguage(Locale locale) {
    context.setLocale(locale);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          DrawerHeader(
            decoration: BoxDecoration(
              color: Colors.blue,
            ),
            child: Text(
              _isAuthenticated ? tr(<span class="hljs-string">'hello'</span>, namedArgs: {<span class="hljs-string">'username'</span>: _username ?? <span class="hljs-string">''</span>}) : tr(<span class="hljs-string">'welcome'</span>),
              style: TextStyle(
                color: Colors.white,
                fontSize: <span class="hljs-number">24</span>,
              ),
            ),
          ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: Icon(Icons.request_page),
              title:Text(tr(<span class="hljs-string">'request_recipe'</span>)),
              onTap: () {

                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) =&gt; RecipeRequestScreen()),

                );
              },
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: <span class="hljs-keyword">const</span> Icon(Icons.person),
              title: Text(tr(<span class="hljs-string">'profile'</span>)),
              onTap: () {
                <span class="hljs-keyword">if</span> (_userId != <span class="hljs-keyword">null</span>) {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) =&gt; ProfileScreen()),
                  );
                }
              },
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: Icon(Icons.logout),
              title: Text(tr(<span class="hljs-string">'logout'</span>)),
              onTap: _logout,

            )
          <span class="hljs-keyword">else</span>
            ListTile(
              leading: Icon(Icons.login),
              title: Text(tr(<span class="hljs-string">'login'</span>)),
              onTap: _navigateToLogin,
            ),
          Divider(),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/en-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'english'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'en'</span>));
    },
          ),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/fr-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'french'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'fr'</span>, <span class="hljs-string">'FR'</span>));
            },
          ),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/ja-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'japanese'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'ja'</span>, <span class="hljs-string">'JP'</span>));
            },
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The <code>CustomDrawer</code> gives users access to different parts of the app and lets them switch languages. It updates its content based on the user's login status. Logged-in users see options like “Request a Recipe,” “Profile,” and “Logout,” while guests only see a “Login” option. It personalizes the user experience by greeting logged-in users with their username.</p>
<p>It also includes a language switcher with flag icons for English, French, and Japanese, powered by the <code>easy_localization</code> package. This allows users to change the app’s language instantly.</p>
<p>On startup, the drawer checks the user's authentication status using <code>SharedPreferences</code> and adjusts the UI accordingly. Navigation is handled with <code>Navigator</code>, enabling smooth transitions to different screens based on the selected menu item.</p>
<h3 id="heading-appbar">AppBar</h3>
<p>The AppBar is the top bar of your app’s screen. It typically contains the app’s title, a back button (if needed), and sometimes actions like search, settings, or a language toggle. In our multilingual recipe app, we’ll use the <code>AppBar</code> to show the current page title and allow easy navigation through the drawer.</p>
<p>In the <code>lib/components/appBar.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-comment">/// <span class="markdown">A customizable AppBar for the Recipe application.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// This AppBar allows for setting a title, actions, a leading widget, </span></span>
<span class="hljs-comment">/// <span class="markdown">centering the title, background color, and elevation.</span></span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeBar</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PreferredSizeWidget</span> </span>{
  <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">List</span>&lt;Widget&gt;? actions;
  <span class="hljs-keyword">final</span> Widget? leading;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> centerTitle;
  <span class="hljs-keyword">final</span> Color? backgroundColor;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> elevation;

  <span class="hljs-keyword">const</span> RecipeBar({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">this</span>.actions,
    <span class="hljs-keyword">this</span>.leading,
    <span class="hljs-keyword">this</span>.centerTitle = <span class="hljs-keyword">true</span>,
    <span class="hljs-keyword">this</span>.backgroundColor,
    <span class="hljs-keyword">this</span>.elevation = <span class="hljs-number">4.0</span>,
    Key? key,
  }) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> AppBar(
      title: Text(title),
      actions: actions,
      leading: leading,
      centerTitle: centerTitle,
      backgroundColor: backgroundColor,
      elevation: elevation,
    );
  }

  <span class="hljs-meta">@override</span>
  Size <span class="hljs-keyword">get</span> preferredSize =&gt; <span class="hljs-keyword">const</span> Size.fromHeight(kToolbarHeight);
}
</code></pre>
<p>The AppBar uses a <code>StatelessWidget</code> since it does not manage any state that changes over time. It implements the <code>PreferredSizeWidget</code> interface, which is necessary for AppBar customization in Flutter.</p>
<p>The constructor of the <code>RecipeBar</code> class takes several parameters to customize the AppBar. The <code>title</code> parameter is required, while the others are optional with default values. The <code>actions</code> parameter allows adding widgets like buttons for login, language switching, or simply navigating to another screen of the app.</p>
<p>In the <code>build</code> method, the AppBar is constructed using the provided parameters. The <code>preferredSize</code> getter returns the preferred height of the AppBar, which is set to the standard toolbar height using <code>kToolbarHeight</code>. This class provides a flexible and reusable AppBar component for the Recipe application, enabling easy customization and consistent UI design across different screens.</p>
<h2 id="heading-fetch-recipes">Fetch Recipes</h2>
<p>In the <code>lib/screens/home.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../components/drawer.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'detail.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _HomeScreenState createState() =&gt; _HomeScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; </span>{
  <span class="hljs-keyword">late</span> Future&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt; _recipesFuture;
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _username;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _checkAuthentication(); <span class="hljs-comment">// Check authentication state when initializing</span>
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>); <span class="hljs-comment">// Check if JWT token is stored</span>
      _username = prefs.getString(<span class="hljs-string">'username'</span>); <span class="hljs-comment">// Get the logged-in user's username from shared preferences</span>
    });
  }

  <span class="hljs-keyword">void</span> _navigateToLogin() {
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-comment">// Logout method</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> ApiService().logout();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _username = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-built_in">String</span> truncateWithEllipsis(<span class="hljs-built_in">int</span> cutoff, <span class="hljs-built_in">String</span> myString) {
    <span class="hljs-keyword">return</span> (myString.length &lt;= cutoff) ? myString : <span class="hljs-string">'<span class="hljs-subst">${myString.substring(<span class="hljs-number">0</span>, cutoff)}</span>...'</span>;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didChangeDependencies() {
    <span class="hljs-keyword">super</span>.didChangeDependencies();
    <span class="hljs-comment">// Initialize _recipesFuture  after context is available</span>
    _recipesFuture = ApiService().fetchRecipes(context);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'recipe_list'</span>)),
        actions: [
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            Padding(
              padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
              child: Center(
                child: Text(tr(<span class="hljs-string">'hello'</span>, namedArgs: {<span class="hljs-string">'username'</span>: _username ?? <span class="hljs-string">''</span>})),
              ),
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            IconButton(
              icon: <span class="hljs-keyword">const</span> Icon(Icons.logout),
              onPressed: _logout,
            )
          <span class="hljs-keyword">else</span>
            TextButton(
              onPressed: _navigateToLogin,
              child: Text(
                tr(<span class="hljs-string">'login'</span>),
                style: <span class="hljs-keyword">const</span> TextStyle(color: Colors.white),
              ),
            ),
        ],
      ),
      drawer: CustomDrawer(),
      body: FutureBuilder&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt;(
        future: _recipesFuture,
        builder: (context, snapshot) {
          <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> Center(child: CircularProgressIndicator());
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
            <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error.toString()}</span>'</span>));
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.data == <span class="hljs-keyword">null</span> || snapshot.data!.isEmpty) {
            <span class="hljs-keyword">return</span> Center(child: Text(tr(<span class="hljs-string">'no_recipe'</span>)));
          }

          <span class="hljs-keyword">return</span> ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (context, index) {
              Recipe recipe = snapshot.data![index];
              <span class="hljs-built_in">String</span> fullDescription = recipe.description.isNotEmpty
                  ? recipe.description.map((d) =&gt; d.children.map((t) =&gt; t.text).join(<span class="hljs-string">' '</span>)).join(<span class="hljs-string">'\n'</span>)
                  : tr(<span class="hljs-string">'no_description'</span>);
              <span class="hljs-built_in">String</span> truncatedDescription = truncateWithEllipsis(<span class="hljs-number">100</span>, fullDescription);

              <span class="hljs-built_in">print</span>(<span class="hljs-string">"Recipe Title: <span class="hljs-subst">${recipe.title}</span>"</span>);
              <span class="hljs-built_in">print</span>(<span class="hljs-string">"Full Description: <span class="hljs-subst">$fullDescription</span>"</span>);

              <span class="hljs-keyword">return</span> GestureDetector(
                onTap: () <span class="hljs-keyword">async</span> {
                  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) =&gt; RecipeDetailPage(recipe: recipe),
                    ),
                  );

                  <span class="hljs-keyword">if</span> (result != <span class="hljs-keyword">null</span> &amp;&amp; result <span class="hljs-keyword">is</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">int</span>&gt;) {
                    setState(() {
                      Recipe updatedRecipe = Recipe(
                        id: recipe.id,
                        title: recipe.title,
                        description: recipe.description,
                        ingredients: recipe.ingredients,
                        likes: result[<span class="hljs-string">'likes'</span>]!,
                        createdAt: recipe.createdAt,
                        updatedAt: recipe.updatedAt,
                        publishedAt: recipe.publishedAt,
                        steps: recipe.steps,
                        commentCount: result[<span class="hljs-string">'commentsCount'</span>]!,
                        comments: recipe.comments,
                        coverImageUrl: recipe.coverImageUrl,
                      );
                      snapshot.data![index] = updatedRecipe;
                    });
                  }
                },
                child: Container(
                  margin: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(horizontal: <span class="hljs-number">10</span>, vertical: <span class="hljs-number">8</span>),
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">10</span>),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(<span class="hljs-number">15</span>),
                    border: Border.all(
                      color: <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xff595959</span>),
                      width: <span class="hljs-number">0.5</span>,
                    ),
                  ),
                  child: Row(
                    children: [
                      Container(
                        height: <span class="hljs-number">80</span>,
                        width: <span class="hljs-number">80</span>,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(<span class="hljs-number">15</span>),
                          image: DecorationImage(
                            image: NetworkImage(recipe.coverImageUrl),
                            fit: BoxFit.cover,
                          ),
                        ),
                      ),
                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">10</span>),
                      Expanded(
                        flex: <span class="hljs-number">3</span>,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              recipe.title.toUpperCase(),
                              style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                            ),
                            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">5</span>),
                            Text(
                              truncatedDescription,
                              style: <span class="hljs-keyword">const</span> TextStyle(color: Color(<span class="hljs-number">0xff595959</span>)),
                            ),
                            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">5</span>),
                            Row(
                              children: [
                                Expanded(
                                  child: Row(
                                    children: [
                                      Text(<span class="hljs-string">'<span class="hljs-subst">${recipe.likes}</span>'</span>),
                                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                                      <span class="hljs-keyword">const</span> Icon(Icons.thumb_up, size: <span class="hljs-number">18</span>, color: Colors.redAccent),
                                    ],
                                  ),
                                ),
                                Expanded(
                                  child: Row(
                                    children: [
                                      Text(<span class="hljs-string">'<span class="hljs-subst">${recipe.commentCount}</span>'</span>),
                                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                                      <span class="hljs-keyword">const</span> Icon(Icons.comment, size: <span class="hljs-number">18</span>, color: Colors.blue),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}
</code></pre>
<p>The <code>HomeScreen</code> mainly displays a list of recipes. It checks if the user is authenticated by looking for a JWT token in shared preferences and sets the authentication state accordingly. If the user is authenticated, it shows a greeting with their username and provides a logout option in the app bar.</p>
<p>The <code>FutureBuilder</code> to fetch recipes from the <code>ApiService</code>. While the data is being fetched, it shows a loading indicator. Once the data is fetched, it displays the list of recipes. Each recipe card includes the title, truncated description, cover image, and the counts of likes and comments.</p>
<p>When a user taps on a recipe, it navigates to a detailed page for that recipe. If the detailed page updates the recipe's likes or comments, the list updates accordingly without reloading the entire screen.</p>
<h2 id="heading-view-recipe">View Recipe</h2>
<p>In the <code>lib/screens/detail.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeDetailPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> Recipe recipe;

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

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RecipeDetailPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RecipeDetailPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _commentController = TextEditingController();
  <span class="hljs-built_in">List</span>&lt;Comment&gt; _comments = [];
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">true</span>;
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _userId;
  <span class="hljs-built_in">int</span> _likes = <span class="hljs-number">0</span>;
  <span class="hljs-built_in">int</span> _commentsCount = <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();
    _initializePage();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _initializePage() <span class="hljs-keyword">async</span> {
    _checkAuthentication();
    _loadComments();
    _likes = widget.recipe.likes;
    _comments = widget.recipe.comments;
    _commentsCount = widget.recipe.commentCount;
    _commentController.addListener(() =&gt; setState(() {}));
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _commentController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>);
      _userId = prefs.getString(<span class="hljs-string">'userId'</span>);
    });
  }

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

  Future&lt;<span class="hljs-keyword">void</span>&gt; _loadComments() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">var</span> comments = <span class="hljs-keyword">await</span> ApiService().fetchComments(widget.recipe.id);
      setState(() {
        _comments = comments;
        _commentsCount = comments.length;
        _isLoading = <span class="hljs-keyword">false</span>;
      });
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">'Error server fetching comments: <span class="hljs-subst">$e</span>'</span>);
      _showError(<span class="hljs-string">'Failed to load comments: <span class="hljs-subst">$e</span>'</span>);
      setState(() =&gt; _isLoading = <span class="hljs-keyword">false</span>);
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _addComment() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_commentController.text.isNotEmpty &amp;&amp; _userId != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">try</span> {
        Comment newComment = <span class="hljs-keyword">await</span> ApiService().postComment(
            _commentController.text, widget.recipe.id, _userId!);

        setState(() {
          _comments.add(newComment);
          _commentsCount++;
          _commentController.clear();
        });

        <span class="hljs-keyword">await</span> ApiService().updateCommentCount(widget.recipe.id, increment: <span class="hljs-keyword">true</span>);
      } <span class="hljs-keyword">catch</span> (e) {
        log(<span class="hljs-string">"Error posting comment: <span class="hljs-subst">$e</span>"</span>);
        _showError(<span class="hljs-string">'Error posting comment: <span class="hljs-subst">$e</span>'</span>);
      }
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _likeRecipe() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> ApiService().likeRecipe(widget.recipe.id);
      setState(() =&gt; _likes++);
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error liking recipe: <span class="hljs-subst">$e</span>"</span>);
      _showError(<span class="hljs-string">'Error liking recipe: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> ApiService().logout();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _userId = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> WillPopScope(
      onWillPop: () <span class="hljs-keyword">async</span> {
        Navigator.pop(context, {
          <span class="hljs-string">'likes'</span>: _likes,
          <span class="hljs-string">'commentsCount'</span>: _commentsCount,
        });
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(widget.recipe.title),
          actions: [
            <span class="hljs-keyword">if</span> (_isAuthenticated)
              IconButton(
                icon: <span class="hljs-keyword">const</span> Icon(Icons.logout),
                onPressed: _logout,
              ),
          ],
        ),
        body: SingleChildScrollView(
          child: Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                <span class="hljs-keyword">if</span> (widget.recipe.coverImageUrl.isNotEmpty)
                  Image.network(
                    widget.recipe.coverImageUrl,
                    width: <span class="hljs-built_in">double</span>.infinity,
                    height: <span class="hljs-number">200</span>,
                    fit: BoxFit.cover,
                  ),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">10</span>),
                Row(
                  children: [
                    Expanded(
                      child: Row(
                        children: [
                          Text(<span class="hljs-string">'<span class="hljs-subst">$_likes</span>'</span>),
                          <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                          IconButton(
                            icon: <span class="hljs-keyword">const</span> Icon(Icons.thumb_up, size: <span class="hljs-number">18</span>, color: Colors.redAccent),
                            onPressed: _likeRecipe,
                          ),
                        ],
                      ),
                    ),
                    Expanded(
                      child: Row(
                        children: [
                          Text(<span class="hljs-string">'<span class="hljs-subst">$_commentsCount</span>'</span>),
                          <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                          <span class="hljs-keyword">const</span> Icon(Icons.comment, size: <span class="hljs-number">18</span>, color: Colors.blue),
                        ],
                      ),
                    ),
                  ],
                ),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                ...widget.recipe.description.map((desc) =&gt;
                    Text(desc.children.map((child) =&gt; child.text).join())),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Ingredients'</span>, style: TextStyle(fontWeight: FontWeight.bold)),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                Text(widget.recipe.ingredients),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Procedure'</span>, style: TextStyle(fontWeight: FontWeight.bold)),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                ...widget.recipe.steps.map((step) =&gt;
                    Text(step.children.map((child) =&gt; child.text).join())),
                <span class="hljs-keyword">if</span> (_isLoading)
                  <span class="hljs-keyword">const</span> CircularProgressIndicator(),
                ..._comments.map((comment) =&gt; ListTile(
                  title: Text(comment.author),
                  subtitle: Text(comment.content),
                  trailing: Text(comment.createdAt.toLocal().toString()),
                )),
                <span class="hljs-keyword">if</span> (_isAuthenticated)
                  Column(
                    children: [
                      TextField(
                        controller: _commentController,
                        decoration: InputDecoration(labelText: tr(<span class="hljs-string">'add_comment'</span>)),
                      ),
                      ElevatedButton(
                        onPressed: _commentController.text.isNotEmpty ? _addComment : <span class="hljs-keyword">null</span>,
                        child: Text(tr(<span class="hljs-string">'submit'</span>)),
                      ),
                    ],
                  )
                <span class="hljs-keyword">else</span>
                  Text(tr(<span class="hljs-string">'login_comment'</span>)),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>This <code>RecipeDetailPage</code> displays detailed information about a selected recipe, including its cover image, likes, comments, ingredients, and procedure. Only authenticated users can comment or like a recipe. During initialization, the page checks if the user is authenticated by reading from local storage. If authenticated, it sets <code>_isAuthenticated</code> to <code>true</code> and retrieves the user's ID, enabling features like adding comments and liking recipes.</p>
<ul>
<li><p><strong>Adding a comment</strong>: The <code>_addComment</code> function posts the new comment to the server, adds it to the local comments list, increments the comment count, and clears the input field.</p>
</li>
<li><p><strong>Liking a recipe</strong>: The <code>_likeRecipe</code> function sends a like request to the server, increases the local like count, and updates the UI.</p>
</li>
</ul>
<p>If the user is not authenticated, they are prompted to log in to leave a comment or interact with the recipe.</p>
<h2 id="heading-create-request-recipe-screen">Create Request Recipe Screen</h2>
<p>In the <code>lib/screens/requestRecipe.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeRequestScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _RecipeRequestScreenState createState() =&gt; _RecipeRequestScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RecipeRequestScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RecipeRequestScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-keyword">final</span> _titleController = TextEditingController();
  <span class="hljs-keyword">final</span> _descriptionController = TextEditingController();
  <span class="hljs-keyword">final</span> ApiService _apiService = ApiService();

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _titleController.dispose();
    _descriptionController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _submitRequest() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      <span class="hljs-keyword">final</span> description = _descriptionController.text;
      <span class="hljs-keyword">final</span> descriptionList = [
        Description(
          type: <span class="hljs-string">'paragraph'</span>,
          children: [
            TextContent(
              type: <span class="hljs-string">'text'</span>,
              text: description,
              bold: <span class="hljs-keyword">false</span>
            ),
          ],
        ),
      ];
      <span class="hljs-keyword">final</span> request = RecipeRequest(
        title: _titleController.text,
        description: descriptionList,
        id: <span class="hljs-number">0</span>,
      );
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> _apiService.submitRecipeRequest(request);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(tr(<span class="hljs-string">'request_successful'</span>))),
        );
        _titleController.clear();
        _descriptionController.clear();
      } <span class="hljs-keyword">catch</span> (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(<span class="hljs-string">'Failed to submit recipe request: <span class="hljs-subst">$e</span>'</span>)),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'request_recipe'</span>)),
      ),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'recipe_title'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> <span class="hljs-string">'Please enter a title'</span>;
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: _descriptionController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'description'</span>)),
                maxLines: <span class="hljs-number">5</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'enter_description'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              ElevatedButton(
                onPressed: _submitRequest,
                child: Text(tr(<span class="hljs-string">'submit_request'</span>)),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>RecipeRequestPage</code> allows authenticated users to submit a request for a new recipe. <code>widget</code> is a statefull widget managed by the <code>_RecipeRequestPageState</code> class. It uses a form with two input fields: one for the recipe title and one for the description. These input fields are controlled by <code>TextEditingController</code> instances, which manage the text entered by the user.</p>
<p>The <code>_submitRequest</code> method handles the form submission. It validates the form fields, constructs a <code>RecipeRequest</code> object with the entered title and description, and sends it to the server using the <code>ApiService</code>. If the submission is successful, a success message is displayed using <code>ScaffoldMessenger</code>. If there is an error, an error message is shown.</p>
<p>The <code>build</code> method constructs the user interface of the screen and displays the form with its inputs.</p>
<h2 id="heading-create-user-profile-screen">Create User Profile Screen</h2>
<p>In the <code>lib/screens/profile.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.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:flutter_recipe_app/screens/requestRecipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _ProfileScreenState createState() =&gt; _ProfileScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ProfileScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ProfileScreen</span>&gt; </span>{
  <span class="hljs-keyword">late</span> Future&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt; _requestedRecipesFuture;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _requestedRecipesFuture = ApiService().fetchUserRequestedRecipes();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'profile'</span>)),
      ),
      body: Column(
        children: [
          Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SizedBox(height: <span class="hljs-number">10</span>),
                Text(
                  tr(<span class="hljs-string">'request_list'</span>),
                  style: TextStyle(fontSize: <span class="hljs-number">16</span>, color: Colors.grey[<span class="hljs-number">600</span>]),
                ),
                SizedBox(height: <span class="hljs-number">20</span>),
                ElevatedButton(
                  onPressed: () {
                    Navigator.pop(context);
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =&gt; RecipeRequestScreen(),
                      ),
                    );
                  },
                  child: Text(tr(<span class="hljs-string">'request_new_recipe'</span>)),
                ),
              ],
            ),
          ),
          Expanded(
            child: FutureBuilder&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt;(
              future: _requestedRecipesFuture,
              builder: (context, snapshot) {
                <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
                  <span class="hljs-keyword">return</span> Center(child: CircularProgressIndicator());
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
                  <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error.toString()}</span>'</span>));
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.data == <span class="hljs-keyword">null</span> || snapshot.data!.isEmpty) {
                  <span class="hljs-keyword">return</span> Center(child: Text(tr(<span class="hljs-string">'no_request_found'</span>)));
                }

                <span class="hljs-keyword">return</span> ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) {
                    RecipeRequest request = snapshot.data![index];
                    <span class="hljs-built_in">String</span> fullDescription = request.description
                        .map((d) =&gt; d.children.map((t) =&gt; t.text).join(<span class="hljs-string">'\n'</span>))
                        .join(<span class="hljs-string">'\n\n'</span>);

                    <span class="hljs-keyword">return</span> Padding(
                      padding: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(horizontal: <span class="hljs-number">40.0</span>),
                      child: ListTile(
                        title: Text(
                          request.title.toUpperCase(),
                          style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                        ),
                        subtitle: Text(fullDescription),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The <code>ProfileScreen</code> class in this Flutter application represents a user's profile page where they can view their requested recipes. When the screen is initialized, it fetches a list of recipes requested by the user by calling the <code>fetchUserRequestedRecipes</code> method from the <code>ApiService</code>. This data is then stored in the <code>_requestedRecipesFuture</code> variable, which is a <code>Future</code> that will eventually hold the list of requested recipes.</p>
<p>In the <code>build</code> method, the screen is constructed using a <code>Scaffold</code> widget.</p>
<p>The main part of the screen is an <code>Expanded</code> widget containing a <code>FutureBuilder</code>. The <code>FutureBuilder</code> widget waits for the <code>_requestedRecipesFuture</code> to complete and then builds the list of requested recipes. If the data is still loading, it shows a <code>CircularProgressIndicator</code>. If there's an error, it displays an error message. And if there are no recipes, it shows a "no request found" message. Otherwise, it displays the list of requested recipes, each rendered as a <code>ListTile</code> with the recipe title and description.</p>
<h2 id="heading-test-the-app">Test the App</h2>
<p>To test the application, connect your device or launch an emulator then run the backend with the command below:</p>
<pre><code class="lang-bash">npm run develop
</code></pre>
<p>And the frontend:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a Flutter and Strapi recipe application where user could register and login to request a recipe from the admin, view and like recipes, or add their comments to a specific recipe.</p>
<p>To improve the application, you can add search functionality, share functionality, or allow users not only to request a recipe but also to create a personal list of recipes they can share with others.</p>
<p>Thanks for reading!</p>
<h3 id="heading-references">References</h3>
<ul>
<li><p>⁠<a target="_blank" href="https://docs.strapi.io/dev-docs/configurations/api-tokens">https://docs.strapi.io/dev-docs/configurations/api-tokens</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://docs.strapi.io/user-docs/settings/API-tokens">https://docs.strapi.io/user-docs/settings/API-tokens</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://docs.strapi.io/dev-docs/backend-customization/examples/authentication">https://docs.strapi.io/dev-docs/backend-customization/examples/authentication</a></p>
</li>
<li><p><a target="_blank" href="https://docs.strapi.io/dev-docs/plugins/i18n">https://docs.strapi.io/dev-docs/plugins/i18n</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://strapi.io/blog/how-to-create-a-refresh-token-feature-in-your-strapi-application">⁠⁠https://strapi.io/blog/how-to-create-a-refresh-token-feature-in-your-strapi-application</a></p>
</li>
<li><p><a target="_blank" href="https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi">https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi</a></p>
</li>
<li><p><a target="_blank" href="https://jwt.io/introduction">https://jwt.io/introduction</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
