...

Flutter + Laravel: Your Guide to Connecting to a Database

Flutter is a framework that helps you build apps for many devices with one set of code. This is why you need a database that can work on all of them. An online database is the best solution. It lets people access their data no matter what phone or computer they use.

In this tutorial, we will cover the following

Laravel setup

First, we are going to start by creating a database, in this example, we will have just two tables:

CREATE DATABASE product_database;
USE product_database;

CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT
);

CREATE TABLE products (
    name VARCHAR(255) NOT NULL,
    category_id INT,
    description TEXT,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

Once we have our database, we will create a new project in Laravel and navigate to the folder

C:\xampp\htdocs 
composer create-project --prefer-dist laravel/laravel productServer "11.*"
cd productServer

Now that we are here, we will update our .env file to contain our database link that should be located at the root of our folder, if .env.example is available, just remove the extension.example

DB_CONNECTION=mysql #update with your database like mysql,sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=product_database
DB_USERNAME=root
DB_PASSWORD=

Now, we will create the migrations from the database. You can delete all the files under /database/migrations to create a clear implementation. We will use the package  https://github.com/kitloong/laravel-migrations-generator to generate them automatically

composer require --dev "kitloong/laravel-migrations-generator"
php artisan migrate:generate

Ensure, you add  $table->timestamps(); to your migrations to create created_at and updated_at fields

Now that we have the migrations, we will run php artisan migrate:fresh To delete all the tables in our database and recreate them, we have to make sure to verify that all is correct. Then we will use https://github.com/reliese/laravel to create the models

composer require reliese/laravel
php artisan vendor:publish --tag=reliese-models
php artisan config:clear
php artisan code:models

You can update public $timestamps = true; to be true, so the dates will be filled automatically

Next, we will install the framework https://github.com/mrmarchone/laravel-auto-crud to create the Controller and API

composer require mrmarchone/laravel-auto-crud --dev
php artisan auto-crud:generate --model=Product --type=api
php artisan auto-crud:generate --model=Category --type=api

Additionally you can update ProductResource to return directly the category name like 'category_name' => $this->category!=null?$this->category->name:'', and add Product::with('category')-> in your controller

Modify project\bootstrap\app.php to include the api.php routes

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Now you can run php artisan route:list to see all the routes and php artisan serve to run the server, if needed run php artisan key:generate, We are done 🙂

Let’s fill our tables

INSERT INTO categories (name, description) VALUES
('Electronics', 'Devices and gadgets, from computers to smartphones.'),
('Books', 'A wide selection of literature, both fiction and non-fiction.'),
('Home Goods', 'Items for the home, including kitchenware, decor, and furniture.');

INSERT INTO products (name, category_id, description) VALUES
('Smartphone', 1, 'A modern smartphone with a high-resolution camera and long battery life.'),
('Laptop', 1, 'A powerful laptop ideal for work and creative tasks.'),
('The Great Gatsby', 2, 'A classic novel by F. Scott Fitzgerald.'),
('Dune', 2, 'A science fiction masterpiece by Frank Herbert.'),
('Coffee Maker', 3, 'An automated drip coffee maker for brewing perfect coffee at home.'),
('Decorative Pillow', 3, 'A soft, stylish pillow to add a touch of comfort to any room.');

Now you can try by accessing to 127.0.0.1:8000/api/products, it will display some fields 🙂

Flutter setup

Let’s create a new project in Flutter. You can follow Flutter setup without Android Studio [Complete Setup Guide] – Algorithms Blog, or open command palette -> New Flutter project.

Creating our Models in Flutter

To create the models, we will create a new folder under /lib called Models, and create two new files product.dart and category.dart, this two classes will have the conversion between Jsons to be able to parse the content.

Add to your pubspec.yaml

dev_dependencies
  build_runner: ^2.4.15
  json_serializable: ^6.7.1
  freezed: ^3.0.6

dependencies:
    json_annotation: ^4.9.0
    http: ^1.5.0

//add category.dart
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'category.freezed.dart';
part 'category.g.dart';

@freezed
@JsonSerializable()
class Category with _$Category {
  @override
  final int id;
  @override
  final String name;
  @override
  final String description;

  const Category({
    this.id=-1,
    required this.name,
    required this.description
  });

  factory Category.fromJson(Map<String, dynamic> json) =>
      _$CategoryFromJson(json);

  Map<String, dynamic> toJson() => _$CategoryToJson(this);

  factory Category.fromJsonString(String text) =>
      _$CategoryFromJson(jsonDecode(text));
}
//product.dart
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:products/Managers/base_manager.dart';

part 'product.freezed.dart';
part 'product.g.dart';

@freezed
@JsonSerializable()
class Product with _$Product implements CustomJsonConvertible  {
  @override
  final int id;
  @override
  final String name;
  @override
  final String description;
  @override
  @JsonKey(name: 'category_name')
  final String categoryName;
  @override
  final int categoryId;

  const Product({
    this.id=-1,
    required this.name,
    required this.description,
    this.categoryName='',
    this.categoryId = -1,
  });

  factory Product.fromJson(Map<String, dynamic> json) =>
      _$ProductFromJson(json);

  Map<String, dynamic> toJson() => _$ProductToJson(this);

  factory Product.fromJsonString(String text) =>
      _$ProductFromJson(jsonDecode(text));

  @override
  Map<String, dynamic> toCustomJson() {
    return {'name': name,'description':description,'category_id':categoryId};
  }
}

Now, you can just run dart run build_runner build or flutter pub run build_runner build --delete-conflicting-outputs and it will generate two extra files. Now, let’s create the Managers for the connection

//endpoint_manager.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

class EndpointManager<T> {
  final bool isTest = true;

  Future<Map<String, dynamic>> fetch(String urlpath) async {
    final url = Uri.parse(urlpath);
    try {
      final response = await http.get(url);
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        return data;
      } else {
        throw Exception(
          'Failed to load post',
        );
      }
    } catch (e) {
      throw Exception('Failed to connect to the server'); 
    }
  }

   Future<Map<String, dynamic>> postData(String urlpath, Map<String, dynamic> body) async {
    final url = Uri.parse(urlpath);
    try {
      final response = await http.post(
        url,
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: jsonEncode(body),
      );
      if (response.statusCode == 200 || response.statusCode == 201) {
        final Map<String, dynamic> data = json.decode(response.body);
        return data;
      } else {
        throw Exception('Failed to post data.');
      }
    } catch (e) {
      throw Exception('Failed to connect to the server');
    }
  }
}

//base_manager.dart
import 'package:flutter/foundation.dart';
import 'package:products/Managers/endpoint_manager.dart';

typedef FromJsonFactory<T> = T Function(Map<String, dynamic> json);

abstract class CustomJsonConvertible {
  Map<String, dynamic> toCustomJson();
}

abstract class BaseManager<T> {
  late EndpointManager<T> _endpointManager;
  final FromJsonFactory<T> _fromJsonFactory;

  Future<void> initialize() async {
     _endpointManager = EndpointManager<T>();
  }

  @protected
  String get baseRoute;

  static String serverUrl = "http://127.0.0.1:8000/";

  BaseManager(this._fromJsonFactory);

  // ignore: avoid_shadowing_type_parameters
  void add<T extends CustomJsonConvertible>(T value) {
    print("entering");
    _endpointManager.postData(serverUrl + baseRoute, value.toCustomJson());
  }

  Future<T> getId(String id) async {
    final map = await _endpointManager.fetch(serverUrl + baseRoute+id);
    return _fromJsonFactory(map["data"] as Map<String, dynamic>);
  }

  Future<List<T>> getAll() async {
    final map = await _endpointManager.fetch(serverUrl+baseRoute);
    if (map["data"] is List) {
      var l =
          (map["data"] as List)
              .map((item) => _fromJsonFactory(item as Map<String, dynamic>))
              .toList();
      return l;
    } else {
      throw Exception('Expected a list in response');
    }
  }
}

//category_manager.dart
import 'package:products/Managers/base_manager.dart';
import 'package:products/Models/category.dart';
class CategoryManager extends BaseManager<Category> {
  static final CategoryManager _instance = CategoryManager._internal();

  CategoryManager._internal() : super(Category.fromJson);

  @override
  String get baseRoute => "api/categories/";

  static CategoryManager current() {
    return _instance;
  }
  
}

//product_manager.dart
import 'package:products/Managers/base_manager.dart';
import 'package:products/Models/product.dart';
class ProductManager extends BaseManager<Product> {
  static final ProductManager _instance = ProductManager._internal();

  ProductManager._internal() : super(Product.fromJson);

  @override
  String get baseRoute => "api/products/";

  static ProductManager current() {
    return _instance;
  }
  
}

//products.dart
import 'package:flutter/material.dart';
import 'package:products/Managers/product_manager.dart';
import 'package:products/Models/product.dart';
import 'package:products/Views/add_product_form.dart';
// The main widget to display the list of products.
class ProductListWidget extends StatefulWidget {
  const ProductListWidget({super.key});

  @override
  State<ProductListWidget> createState() => _ProductListWidgetState();
}

class _ProductListWidgetState extends State<ProductListWidget> {
  List<Product>? _products;

  @override
  void initState() {
    super.initState();
    _fetchProducts();
  }

  // Method to fetch the products and update the state.
  void _fetchProducts() async {
    final products = await ProductManager.current().getAll();
    setState(() {
      _products = products;
    });
  }

  // A new method to show the modal for adding a new product.
  void _showAddProductModal() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return const Dialog(
          child: AddProductForm(),
        );
      },
    ).then((result) {
      // If a product was successfully added (the result is not null),
      // refresh the product list.
      if (result != null) {
        _fetchProducts();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_products == null) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        centerTitle: true,
      ),
      body: _products!.isEmpty
          ? const Center(
              child: Text('No products found.'),
            )
          : GridView.builder(
              padding: const EdgeInsets.all(16.0),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 10.0,
                mainAxisSpacing: 10.0,
                childAspectRatio: .8, // Adjust to fit card content
              ),
              itemCount: _products!.length,
              itemBuilder: (context, index) {
                final product = _products![index];
                return Card(
                  elevation: 4.0,
                  child: Column(
                    children: [
                      
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Text(
                          product.name,
                          style: const TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 16,
                          ),
                          textAlign: TextAlign.center,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
                        child: Text(
                          product.description,
                          style: const TextStyle(fontSize: 12),
                          textAlign: TextAlign.center,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                       Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
                        child: Text(
                          product.categoryName,
                          style: const TextStyle(fontSize: 12),
                          textAlign: TextAlign.center,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _showAddProductModal,
        child: const Icon(Icons.add),
      ),
    );
  }
}
//add_product_form.dart
import 'package:flutter/material.dart';
import 'package:products/Managers/category_manager.dart';
import 'package:products/Managers/product_manager.dart';
import 'package:products/Models/category.dart';
import 'package:products/Models/product.dart';

// A new widget for the product creation form.
class AddProductForm extends StatefulWidget {
  const AddProductForm({super.key});

  @override
  State<AddProductForm> createState() => _AddProductFormState();
}

class _AddProductFormState extends State<AddProductForm> {
  final _formKey = GlobalKey<FormState>();
  String _name = '';
  String _description = '';
  int? _selectedCategoryId;
  List<Category>? _categories;

  @override
  void initState() {
    super.initState();
    _fetchCategories();
  }

  void _fetchCategories() async {
    final categories = await CategoryManager.current().getAll();
    setState(() {
      _categories = categories;
      _selectedCategoryId = categories.first.id; // Pre-select the first category
    });
  }

  void _submitForm() async {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      final newProduct = Product(
        id: -1,
        name: _name,
        description: _description,
        categoryId: _selectedCategoryId!,
      );
      ProductManager.current().add(newProduct);
      if (mounted) {
        Navigator.of(context).pop(true);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    
    if (_categories == null) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_categories!.isEmpty) {
      return const Center(child: Text('No categories available to add products.'));
    }
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'Add New Product',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            TextFormField(
              decoration: const InputDecoration(labelText: 'Product Name'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter a product name';
                }
                return null;
              },
              onSaved: (value) {
                _name = value!;
              },
            ),
            const SizedBox(height: 8),
            TextFormField(
              decoration: const InputDecoration(labelText: 'Description'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter a description';
                }
                return null;
              },
              onSaved: (value) {
                _description = value!;
              },
            ),
            const SizedBox(height: 16),
            DropdownButtonFormField<int>(
              decoration: const InputDecoration(labelText: 'Category'),
              value: _selectedCategoryId,
              items: _categories!.map((Category category) {
                return DropdownMenuItem<int>(
                  value: category.id,
                  child: Text(category.name),
                );
              }).toList(),
              onChanged: (int? newValue) {
                setState(() {
                  _selectedCategoryId = newValue;
                });
              },
              validator: (value) {
                if (value == null) {
                  return 'Please select a category';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _submitForm,
              child: const Text('Add Product'),
            ),
          ],
        ),
      ),
    );
  }
}


Finally update main.dart, initialize the next classes:
ProductManager.current().initialize();
CategoryManager.current().initialize();

And return home: const ProductListWidget(), 

To be able to connect our browser with the local server, we have to deactivate some security restrictions, add this in your json launch in VS or run with flutter run -d chrome --web-browser-flag "--disable-web-security" and to run in Android, we just need to run adb reverse tcp:8000 tcp:8000 where the port is the one where we are running Laravel

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "mvp_platform",
            "request": "launch",
            "type": "dart"
        },
        {
            "name": "mvp_platform (profile mode)",
            "request": "launch",
            "type": "dart",
            "flutterMode": "profile"
        },
        {
            "name": "mvp_platform (release mode)",
            "request": "launch",
            "type": "dart",
            "flutterMode": "release"
        },
        {
            "name": "Chrome with Web Security Disabled",
            "request": "launch",
            "type": "dart",
            "deviceId": "chrome",
            "flutterMode": "debug",
            "args": [
                "--web-browser-flag",
                "--disable-web-security"
            ]
        }
    ]
}

Happy coding 🙂 !

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.