Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Microsoft Graph CLI to be retired

      September 2, 2025

      The state of DevOps and AI: Not just hype

      September 1, 2025

      A Breeze Of Inspiration In September (2025 Wallpapers Edition)

      August 31, 2025

      10 Top Generative AI Development Companies for Enterprise Node.js Projects

      August 30, 2025

      Spec-driven development with AI: Get started with a new open source toolkit

      September 2, 2025

      Should the CSS light-dark() Function Support More Than Light and Dark Values?

      September 2, 2025

      A Behind-the-Scenes Look at the New Jitter Website

      September 2, 2025

      The Modern Job Hunt: Part 1

      September 2, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Enhanced Queue Job Control with Laravel’s ThrottlesExceptions failWhen() Method

      September 2, 2025
      Recent

      Enhanced Queue Job Control with Laravel’s ThrottlesExceptions failWhen() Method

      September 2, 2025

      August report 2025

      September 2, 2025

      Fake News Detection using Python Machine Learning (ML)

      September 1, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Lenovo Legion Go 2 confirmed with Ryzen Z2 Extreme, 1200p OLED 144Hz display & 74Wh battery

      September 2, 2025
      Recent

      Lenovo Legion Go 2 confirmed with Ryzen Z2 Extreme, 1200p OLED 144Hz display & 74Wh battery

      September 2, 2025

      How to Open Ports in Firewall on Windows Server

      September 2, 2025

      Google TV Remote Not Working? 5 Quick Fixes

      September 2, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Build a Multilingual Social Recipe Application with Flutter and Strapi

    How to Build a Multilingual Social Recipe Application with Flutter and Strapi

    April 9, 2025
    How to Build a Multilingual Social Recipe Application with Flutter and Strapi

    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 interfaces for mobile, web, and desktop from a single codebase.

    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.

    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.

    In this application, users will be able to view recipes, request a specific recipe, share their favorite recipes, and like or comment on recipes.

    Table of Contents

    1. Prerequisites

    2. Demo

    3. Create Models

    4. Add Languages and Enable Internationalization in Strapi

    5. Add Recipe Content

      • Add Recipe English Content

      • Add Recipe French Content

      • Add Recipe Japanese Content

    6. Generate API Token and Set permissions

      • Set User Roles and Permissions
    7. Set up Flutter

      • Project Structure
    8. Install Packages

      • Add Assets

      • Taking a look at main.dart

    9. Add Environment Variables

    10. Create Models

      • 1. RecipeRequest

      • 2. Step

      • 3. Description

      • 4. TextContent

      • 5. Comment

      • 6. Recipe

    11. Create Services

      • 1. Class Variables

      • 2. Helper Methods

      • 3. User Operations

      • 4. Data Fetching and Manipulation

    12. Authorization and Authentication

      • Registration

      • Login

    13. Build App Components

      • Drawer

      • AppBar

    14. Fetch Recipes

    15. View Recipe

    16. Create Request Recipe Screen

    17. Create User Profile Screen

    18. Test the App

    19. Conclusion

    20. References

    Prerequisites

    To follow along with this tutorial, make sure you have:

    • Node.js installed.

    • Basic knowledge of Flutter

    • Basic understanding of Strapi with this quick guide

    Demo

    Here’s what you will be building in the tutorial:

    1. Authentication and Authorization: Demo

    2. Comment and Likes: Demo

    3. Request recipe: Demo

    4. Language Switch: Demo

    You can get the full code of the application from this GitHub repository.

    Create Models

    Once you have set up a Strapi project with this quick guide, create two models, Recipe and RecipeRequest, in the Strapi admin panel.

    A recipe typically has the following elements:

    • Title: text which represents the title of the recipe

    • Ingredients: text which represent the of ingredients of the recipe

    • Likes: int which represent the number of likes

    • Author: relation which represent the author of the recipe

    • Comments: relation which represent the list of comments of a specific recipe

    • Steps: rich text which represents the main content of the recipe

    • Description: rich text which represents a description of what the recipe is like

    • Comment Count: int which represents the number of comment a recipe has

    • Cover Image: media which represents the cover image of the recipe

    recipe model

    Make sure to enable internationalization for Recipe Content Type when you create it:

    enable internationalization

    A recipe request typically has:

    • Title, which is text that represents the title of the request

    • Description, which is rich text that represents the content of the request

    recipe request model

    A comment typical has:

    • Author, which is a relation that represents the author of the comment

    • Content, which is text that represents the content of the comments

    • Date, which is a date that represents the published date of the comment

    comment model

    The user will also have 4 new fields:

    additional user fields

    Add Languages and Enable Internationalization in Strapi

    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.

    Add Recipe Content

    Next, you will populate some recipe data in English, French, and Japanese.

    Add Recipe English Content

    Since English is the default language, go to Content manager, then select Recipe, and then select Create new entry:

    list of added recipes

    Add Recipe French Content

    For French, navigate to Settings, select Internationalization, and then under global settings click on Add new locale. Here you will add the French language.

    french language config

    Back to the Content manager, click on recipe and select the French language in the top right corner. Then choose the Create recipe entry in French.

    french model version

    Add Recipe Japanese Content

    Navigate back to Settings and Internationalization, and under global settings again click on Add new locale. Now you will add the Japanese language.

    japanese language config

    Back to the Content manager, click on recipe and select the Japanese language in the top right corner. Then select Create new entry in Japanese.

    Japenese recipe list

    Generate API Token and Set permissions

    Once you’ve added the content for the various languages, it’s time to create your API and set the necessary permissions.

    To do this, navigate to Settings, then API Tokens, and then Create API Token. Add the details of your key there.

    API token creation

    • Token duration: choose Unlimited

    • Token Type: Custom. The custom type allows you to specify permission for certain entities.

    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:

    enable permission for recipe request

    f5518d2e-5200-40b3-9b74-ed0b0adeeabb

    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

    Set User Roles and Permissions

    You’ll also need to set the user roles and permissions using the User and Permission Plugin. It allows you to manage what both authenticated and non-authenticated users can do in your application.

    Head to the Settings section of the dashboard and go to Roles under the User and Permission plugin.

    We have two types of users:

    • Authenticated users

    • Public users

    8023d7c4-c07b-43dc-ba00-89a958bc0672

    Select the authenticated users and give them the following permissions for:

    Comment:

    enable permission for comments

    Recipe:

    enable authorized user to perdorm action on recipe model

    Request-recipe:

    enable permission for recipe request model

    Also select all for Content-type builder, i18n, and Upload and then save.

    Public users can only read recipes and comments:

    limit comment operation for public users

    limit recipe operations for public user

    Set Up Flutter

    Once you have set up Flutter in your environment, run the following command to bootstrap a new application in your favorite directory:

    flutter create flutter_recipe_app
    

    To see your app in action, you need to run it on a mobile device. You can either:

    • Use an emulator (a virtual Android or iOS device that runs on your computer), or

    • Connect a physical device (like your smartphone) to your computer with a USB cable.

    Once your emulator or device is ready, navigate into the newly created project folder:

    flutter run
    

    This command builds the app and starts it on your connected device or emulator.

    flutter starter app

    Project Structure

    Now let’s look at the file structure of the project:

    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
    

    The structure is organized as follows:

    • .dart_tool/: Contains Dart tools and build outputs.

    • .idea/: IDE-specific settings.

    • android/: Android-specific project files, including custom assets like images and translations.

    • build/: Generated files from the build process.

    • ios/: iOS-specific project files.

    • lib/: The main source directory for Dart code, which includes:

      • components/: Reusable widgets or UI components like appBar and drawer.

      • models/: Data models for your application, like recipe.

      • screens/: Individual screens of the app, such as the recipe details, home, login, profile, request recipe and signUp screens of the app

      • utils/: Utilities and helper functions, like server2.dart for the server communication logic.

    • main.dart: The entry point of the Flutter application.

    • test/: Directory for test files.

    • .env: Environment-specific variables file.

    This setup is typical for a moderately complex Flutter application, segregating functionality into manageable, logical sections for better organization and maintainability.

    Install Packages

    In this tutorial, we’re using five main packages:

    • flutter_dotenv: to manage environment variables

    • http: to handle HTTP requests and interact with Strapi REST API

    • shared_preferences: persists key-value data on the device like user login tokens

    • provider: for state management and updating your UI reactively when the underlying state changes

    • easy_localization: for managing translations and locale data. It supports both JSON and YAML file formats for defining translations.

    In your pubspec.yaml file, add the following lines:

    <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>
    

    Then run the command below to install the packages:

    flutter pub get
    

    Add Assets

    Add the path to your assets in your pubspec.yaml file found at the root of your project:

    <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>
    

    The translations folder contains the list of your translations while the images folder hosts the photos of your application.

    Taking a look at main.dart

    In the main.dart file, you need to set up your localization, load environment variables, and a list of providers for dependency injection:

    <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<<span class="hljs-keyword">void</span>> 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: (_) => 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) => RecipeRequestScreen(),
              <span class="hljs-string">'/login'</span>: (context) => LoginScreen(),
              <span class="hljs-string">'/register'</span>: (context) => RegisterScreen(),
              <span class="hljs-string">'/home'</span>: (context) => HomeScreen(), <span class="hljs-comment">// Implement HomeScreen</span>
            },
          ),
        );
      }
    }
    

    From the code snippet above, the WidgetsFlutterBinding.ensureInitialized() ensures that all Flutter bindings are initialized before any other operations and the EasyLocalization.ensureInitialized() initializes the EasyLocalization package to handle translations.

    Load the environment variables with dotenv.load(fileName: ".env") to read variables from the .env file. The runApp function wraps the MyApp widget with the EasyLocalization widget, which is configured to support English (en), French (fr_FR), and Japanese (ja_JP) locales. The path for translation files is set to 'assets/translations', and the fallback locale is set to English.

    It also creates the main routes of the recipe application and sets home as the initial route.

    Add Environment Variables

    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 flutter_dotenv package you installed earlier. Create an .env file in your root directory and add your environment variables:

    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
    
    • BASE_URL: This is the base URL for your Strapi backend server. The /api 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.

    • USERS_ENDPOINT: This endpoint typically handles login operations where existing users authenticate by submitting their credentials.

    • USERS_ENDPOINT_REG: This is the registration endpoint for new users.

    • ACCESS_TOKEN: This is the API token you created earlier which is used for authenticating API requests.

    • RECIPE_ENDPOINT: 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.

    • COMMENT_ENDPOINT: This endpoint manages comments related to recipes.

    • R_REQUEST_ENDPOINT: This endpoint manages requests related to recipes.

    Create Models

    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 lib/models/recipe.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><Description> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><Description> descriptionList = (attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [])
            .map((desc) => 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> toJson() {
        <span class="hljs-keyword">return</span> {
          <span class="hljs-string">'title'</span>: title,
          <span class="hljs-string">'description'</span>: description.map((desc) => 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><TextContent> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><TextContent> parsedChildren = childrenList.map((child) => 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> toJson() {
        <span class="hljs-keyword">return</span> {
          <span class="hljs-string">'type'</span>: type,
          <span class="hljs-string">'children'</span>: children.map((child) => 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><TextContent> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><TextContent> parsedChildren = childrenList.map((child) => 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> toJson() {
        <span class="hljs-keyword">return</span> {
          <span class="hljs-string">'type'</span>: type,
          <span class="hljs-string">'children'</span>: children.map((child) => 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> ?? {};
        <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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><Description> 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><Step> 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><Comment> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> ?? {};
    
        <span class="hljs-comment">// Parse descriptions</span>
        <span class="hljs-built_in">List</span><Description> descriptionList = [];
        <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'description'</span>] != <span class="hljs-keyword">null</span> && 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) => Description.fromJson(desc)).toList();
        }
    
        <span class="hljs-comment">// Parse steps</span>
        <span class="hljs-built_in">List</span><Step> stepsList = [];
        <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'steps'</span>] != <span class="hljs-keyword">null</span> && 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) => Step.fromJson(step)).toList();
        }
    
        <span class="hljs-comment">// Parse comments</span>
        <span class="hljs-built_in">List</span><Comment> commentList = [];
        <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'comments'</span>] != <span class="hljs-keyword">null</span> && attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] != <span class="hljs-keyword">null</span> && 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) => Comment.fromJson(comment)).toList();
        }
    
        <span class="hljs-comment">// var attr = json['attributes'] as Map<String, dynamic>;</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> && 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><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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) => 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) => step.toJson()).toList(),
          <span class="hljs-string">'commentCount'</span>: commentCount,
          <span class="hljs-string">'comments'</span>: comments.map((comment) => comment.toJson()).toList(),
          <span class="hljs-string">'cover'</span>: coverImageUrl
        };
      }
    }
    

    Let’s go over this code piece by piece, as it’s a lot:

    1. RecipeRequest

    The RecipeRequest class represents the class that allows a user to request a recipe. It has three properties (id, title, and a list of Description objects as defined in the Strapi backend) with 2 methods:

    • fromJson: to convert JSON data into a RecipeRequest object, including parsing a list of descriptions.

    • toJson: to convert a RecipeRequest object back to JSON.

    2. Step

    Represents the cooking steps in a recipe. It contains a list of Textcontent objects, and each Step object has a type, level, and children as it is a richtext type. It also has two methods:

    • fromJson: to parse JSON to create a Step object.

    • toJson: to convert a Step object back to JSON.

    3. Description

    This class also contains a list of TextContent objects (children). Each Description object also has a type and an optional level to indicate hierarchical structure. It has two methods, too:

    • fromJson: to convert JSON into a Description object.

    • toJson: to serialise a Description object to JSON.

    4. TextContent

    This class is designed to represent individual pieces of text within larger structures. Each TextContent object can contain a string of text (text), the type of text (type), and an optional boolean to indicate whether the text is bold (bold)

    • fromJson: Parses JSON into a TextContent object.

    • toJson: Converts a TextContent object back to JSON.

    5. Comment

    As the name indicates, this represents a comment written by a use. It has three properties: the comment content, author, and createdAt. Like others, it also includes two methods:

    • fromJson: to extract and construct a Comment object from JSON, including parsing author data.

    • toJson: to serializes a Comment object to JSON.

    6. Recipe

    Finally, there is the Recipe 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:

    • fromJson: to build a Recipe object from JSON data. This includes parsing lists of descriptions, steps, and comments. It also adjusts the image URL to ensure it is absolute.

    • toJson: to convert the Recipe object to JSON format.

    As you can see, each class is designed to handle specific parts of the recipe data, with fromJson methods to parse JSON into Dart objects and toJson methods to serialize Dart objects back to JSON.

    Create Services

    Now that your environment variables are set up, you can create different services for communicating with the server. In your lib/utils/server.dart file, add the code below:

    <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<<span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>>> _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<<span class="hljs-built_in">String?</span>> 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<<span class="hljs-keyword">void</span>> 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<<span class="hljs-keyword">void</span>> 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<<span class="hljs-keyword">void</span>> setUserData(<span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>> 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<<span class="hljs-keyword">void</span>> 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<http.Response> 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<http.Response> 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<<span class="hljs-keyword">void</span>> 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<<span class="hljs-built_in">List</span><Recipe>> 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>&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><<span class="hljs-built_in">dynamic</span>> dataList = jsonResponse[<span class="hljs-string">'data'</span>];
          <span class="hljs-built_in">List</span><Recipe> 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<<span class="hljs-built_in">List</span><Comment>> 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>&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> && jsonData.containsKey(<span class="hljs-string">'data'</span>)) {
              <span class="hljs-built_in">List</span><<span class="hljs-built_in">dynamic</span>> data = jsonData[<span class="hljs-string">'data'</span>];
              <span class="hljs-keyword">return</span> data.map<Comment>((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<Comment> 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<<span class="hljs-keyword">void</span>> 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 < <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<<span class="hljs-keyword">void</span>> 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<<span class="hljs-keyword">void</span>> 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> && 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<<span class="hljs-built_in">List</span><RecipeRequest>> 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><<span class="hljs-built_in">dynamic</span>> data = jsonResponse[<span class="hljs-string">'data'</span>];
            <span class="hljs-keyword">return</span> data.map((json) => 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>;
        }
      }
    }
    

    The ApiService 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.

    There are four main entities:

    1. Class Variables

    • baseUrl is the base URL.

    • registerEndpoint, loginEndpoint, recipeEndpoint, commentEndpoint, requestEndpoint are the specific endpoints for registration, login, recipes, comments, and requests.

    • accessToken is the token used for API authentication.

    2. Helper Methods

    • _getHeaders prepares the headers for HTTP requests and it optionally includes a JWT token if includeJwt is true.

    • getJwt retrieves the JWT token from shared preferences.

    • setJwt and setUserData store the JWT token and user data (ID and username) in shared preferences once the user logs in.

    • removeJwt and removeUserData remove the JWT token and user data from shared preferences, respectively, and log the user out.

    3. User Operations

    • register registers a new user with the given username, email, and password. It sends a POST request to the registration endpoint with the user details.

    • login logs in a user with the given email and password. If successful, it stores the received JWT token and user data.

    • logout logs out the user by removing the JWT token and user data from shared preferences.

    4. Data Fetching and Manipulation

    • fetchRecipes fetches a list of recipes based on the current locale (language) from the backend. It handles parsing the JSON response into a list of Recipe objects.

    • fetchComments fetches comments for a specific recipe by its ID. It populates the comment_author field and returns a list of Comment objects.

    • postComment posts a new comment on a specific recipe. It sends the comment content, recipe ID, and author ID to the backend.

    • updateCommentCount updates the comment count for a specific recipe. It first fetches the current count, modifies it, and then updates it on the backend.

    • likeRecipe: Increments the like count for a specific recipe by fetching the current count, adding one, and updating the backend.

    • submitRecipeRequest submits a new recipe request to the backend. It sends the request data wrapped in a data object.

    • fetchUserRequestedRecipes fetches a list of recipes requested by a specific user from the backend.

    Authorization and Authentication

    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.

    On the other hand, authentication is the process of validating and verifying a user.

    There are many Authorization and Authentication methods, but in this tutorial we’ll use password-based authentication and an API Key for authorization.

    Registration

    In the lib/screen/signUp.dart file, add the code below:

    <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() => _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><<span class="hljs-title">RegisterScreen</span>> </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<FormState>();
      <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<<span class="hljs-keyword">void</span>> _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<ApiService>(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: (_) => LoginScreen()),
            );
          } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">// Handle error</span>
            showDialog(
              context: context,
              builder: (context) => 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: (_) => 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>),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    This code provides a user-friendly registration interface for the recipe application. The RegisterScreen class is a stateful widget that manages the registration process.

    The _register method validates the form and calls the register method from the ApiService. 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.

    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.

    Upon submission, the form displays a loading indicator while the app communicates with the server to register the user.

    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.

    Login

    <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() => _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><<span class="hljs-title">LoginScreen</span>> </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<FormState>();
      <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<<span class="hljs-keyword">void</span>> _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<ApiService>(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) => 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: (_) => 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>),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    The LoginScreen 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.

    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 GlobalKey to manage its state and ensures that the text controllers are properly disposed of when the widget is removed from the tree.

    Build App Components

    Drawer

    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.

    In our app, the drawer will include links to the Request recipe screen, Profile, Logout, and languages for authenticated users.

    In the lib/components/drawer.dart file, add the code below:

    <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() => _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><<span class="hljs-title">CustomDrawer</span>> </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<<span class="hljs-keyword">void</span>> _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<<span class="hljs-keyword">void</span>> _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) => 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) => 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>));
                },
              ),
            ],
          ),
        );
      }
    }
    

    The CustomDrawer 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.

    It also includes a language switcher with flag icons for English, French, and Japanese, powered by the easy_localization package. This allows users to change the app’s language instantly.

    On startup, the drawer checks the user’s authentication status using SharedPreferences and adjusts the UI accordingly. Navigation is handled with Navigator, enabling smooth transitions to different screens based on the selected menu item.

    AppBar

    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 AppBar to show the current page title and allow easy navigation through the drawer.

    In the lib/components/appBar.dart file, add the code below:

    <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><Widget>? 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 => <span class="hljs-keyword">const</span> Size.fromHeight(kToolbarHeight);
    }
    

    The AppBar uses a StatelessWidget since it does not manage any state that changes over time. It implements the PreferredSizeWidget interface, which is necessary for AppBar customization in Flutter.

    The constructor of the RecipeBar class takes several parameters to customize the AppBar. The title parameter is required, while the others are optional with default values. The actions parameter allows adding widgets like buttons for login, language switching, or simply navigating to another screen of the app.

    In the build method, the AppBar is constructed using the provided parameters. The preferredSize getter returns the preferred height of the AppBar, which is set to the standard toolbar height using kToolbarHeight. This class provides a flexible and reusable AppBar component for the Recipe application, enabling easy customization and consistent UI design across different screens.

    Fetch Recipes

    In the lib/screens/home.dart file, add the code below:

    <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() => _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><<span class="hljs-title">HomeScreen</span>> </span>{
      <span class="hljs-keyword">late</span> Future<<span class="hljs-built_in">List</span><Recipe>> _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<<span class="hljs-keyword">void</span>> _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<<span class="hljs-keyword">void</span>> _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 <= 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<<span class="hljs-built_in">List</span><Recipe>>(
            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) => d.children.map((t) => 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) => RecipeDetailPage(recipe: recipe),
                        ),
                      );
    
                      <span class="hljs-keyword">if</span> (result != <span class="hljs-keyword">null</span> && result <span class="hljs-keyword">is</span> <span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">int</span>>) {
                        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),
                                        ],
                                      ),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            },
          ),
        );
      }
    }
    

    The HomeScreen 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.

    The FutureBuilder to fetch recipes from the ApiService. 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.

    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.

    View Recipe

    In the lib/screens/detail.dart file, add the code below:

    <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() => _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><<span class="hljs-title">RecipeDetailPage</span>> </span>{
      <span class="hljs-keyword">final</span> _commentController = TextEditingController();
      <span class="hljs-built_in">List</span><Comment> _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<<span class="hljs-keyword">void</span>> _initializePage() <span class="hljs-keyword">async</span> {
        _checkAuthentication();
        _loadComments();
        _likes = widget.recipe.likes;
        _comments = widget.recipe.comments;
        _commentsCount = widget.recipe.commentCount;
        _commentController.addListener(() => setState(() {}));
      }
    
      <span class="hljs-meta">@override</span>
      <span class="hljs-keyword">void</span> dispose() {
        _commentController.dispose();
        <span class="hljs-keyword">super</span>.dispose();
      }
    
      Future<<span class="hljs-keyword">void</span>> _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<<span class="hljs-keyword">void</span>> _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(() => _isLoading = <span class="hljs-keyword">false</span>);
        }
      }
    
      Future<<span class="hljs-keyword">void</span>> _addComment() <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">if</span> (_commentController.text.isNotEmpty && _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<<span class="hljs-keyword">void</span>> _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(() => _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<<span class="hljs-keyword">void</span>> _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) =>
                        Text(desc.children.map((child) => 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) =>
                        Text(step.children.map((child) => child.text).join())),
                    <span class="hljs-keyword">if</span> (_isLoading)
                      <span class="hljs-keyword">const</span> CircularProgressIndicator(),
                    ..._comments.map((comment) => 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>)),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    

    This RecipeDetailPage 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 _isAuthenticated to true and retrieves the user’s ID, enabling features like adding comments and liking recipes.

    • Adding a comment: The _addComment function posts the new comment to the server, adds it to the local comments list, increments the comment count, and clears the input field.

    • Liking a recipe: The _likeRecipe function sends a like request to the server, increases the local like count, and updates the UI.

    If the user is not authenticated, they are prompted to log in to leave a comment or interact with the recipe.

    Create Request Recipe Screen

    In the lib/screens/requestRecipe.dart file, add the code below:

    <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() => _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><<span class="hljs-title">RecipeRequestScreen</span>> </span>{
      <span class="hljs-keyword">final</span> _formKey = GlobalKey<FormState>();
      <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<<span class="hljs-keyword">void</span>> _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>)),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    The RecipeRequestPage allows authenticated users to submit a request for a new recipe. widget is a statefull widget managed by the _RecipeRequestPageState 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 TextEditingController instances, which manage the text entered by the user.

    The _submitRequest method handles the form submission. It validates the form fields, constructs a RecipeRequest object with the entered title and description, and sends it to the server using the ApiService. If the submission is successful, a success message is displayed using ScaffoldMessenger. If there is an error, an error message is shown.

    The build method constructs the user interface of the screen and displays the form with its inputs.

    Create User Profile Screen

    In the lib/screens/profile.dart file, add the code below:

    <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() => _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><<span class="hljs-title">ProfileScreen</span>> </span>{
      <span class="hljs-keyword">late</span> Future<<span class="hljs-built_in">List</span><RecipeRequest>> _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) => RecipeRequestScreen(),
                          ),
                        );
                      },
                      child: Text(tr(<span class="hljs-string">'request_new_recipe'</span>)),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: FutureBuilder<<span class="hljs-built_in">List</span><RecipeRequest>>(
                  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) => d.children.map((t) => t.text).join(<span class="hljs-string">'n'</span>))
                            .join(<span class="hljs-string">'nn'</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),
                          ),
                        );
                      },
                    );
                  },
                ),
              ),
            ],
          ),
        );
      }
    }
    

    The ProfileScreen 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 fetchUserRequestedRecipes method from the ApiService. This data is then stored in the _requestedRecipesFuture variable, which is a Future that will eventually hold the list of requested recipes.

    In the build method, the screen is constructed using a Scaffold widget.

    The main part of the screen is an Expanded widget containing a FutureBuilder. The FutureBuilder widget waits for the _requestedRecipesFuture to complete and then builds the list of requested recipes. If the data is still loading, it shows a CircularProgressIndicator. 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 ListTile with the recipe title and description.

    Test the App

    To test the application, connect your device or launch an emulator then run the backend with the command below:

    npm run develop
    

    And the frontend:

    npm run dev
    

    Conclusion

    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.

    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.

    Thanks for reading!

    References

    • ⁠https://docs.strapi.io/dev-docs/configurations/api-tokens

    • ⁠⁠https://docs.strapi.io/user-docs/settings/API-tokens

    • ⁠⁠https://docs.strapi.io/dev-docs/backend-customization/examples/authentication

    • https://docs.strapi.io/dev-docs/plugins/i18n

    • ⁠⁠⁠⁠https://strapi.io/blog/how-to-create-a-refresh-token-feature-in-your-strapi-application

    • https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi

    • https://jwt.io/introduction

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleI challenged myself to use a cheap Android 15 tablet over my iPad Pro – and didn’t regret it
    Next Article Xbox Games Showcase 2025 revealed for June 8, 2025: Outer Worlds 2 deep dive confirmed, as Xbox gears up for the future

    Related Posts

    Development

    Malicious npm Package nodejs-smtp Mimics Nodemailer, Targets Atomic and Exodus Wallets

    September 3, 2025
    Development

    Silver Fox Exploits Microsoft-Signed WatchDog Driver to Deploy ValleyRAT Malware

    September 3, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    Microsoft shares a glimpse of rejected Windows 11 Start menu designs

    Operating Systems

    How to more efficiently study complex treatment interactions

    Artificial Intelligence

    CVE-2025-5353 – Ivanti Workspace Control SQL Credential Decryption Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    CVE-2025-47851 – JetBrains TeamCity Stored XSS Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    Highlights

    CVE-2025-47682 – Cozy Vision Technologies Pvt. Ltd. SMS Alert Order Notifications – WooCommerce SQL Injection

    May 12, 2025

    CVE ID : CVE-2025-47682

    Published : May 12, 2025, 7:15 p.m. | 27 minutes ago

    Description : Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’) vulnerability in Cozy Vision Technologies Pvt. Ltd. SMS Alert Order Notifications – WooCommerce allows SQL Injection.This issue affects SMS Alert Order Notifications – WooCommerce: from n/a through 3.8.2.

    Severity: 9.3 | CRITICAL

    Visit the link for more details, such as CVSS details, affected products, timeline, and more…

    Windows 11’s Start menu gets a bigger, new look: Get it today with these simple steps

    June 11, 2025

    Zencoder acquires Machinet to further improve its AI coding agents

    April 24, 2025

    Urgent Veeam Update: Critical RCE CVE-2025-23121 (CVSS 9.9) & Two Other Flaws Threaten Backup Servers

    June 17, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.