Upload Images To REST API In Flutter Using http package

FlutterImage Upload

File/Image uploading via mobile application is the most common feature in this world of mobile applications. In this tutorial, I am going to explain how you can upload any images to your backend REST API using the http package.

The Approach To The Problem.

There are mainly two ways you can upload the image to your server,

  1.  Converting the image file to base64 encoded string, which gives you ASCII(textual) representation of the file then you send it as request payload.
  2.  Another way is to send it as binary data via HTTP request as multipart/form-data which is a standard way of uploading file/images.

In this tutorial, I will be using the second approach.

Why Not Base64?

Base64 way is very easy to use but easy does not mean it is always good. Here are some reasons due to which I think using it should be avoided most of the time.

  1. Base64 increases the size of the data transferred by 33% which means more time required to transfer the data.
  2. If you want to use Amazon S3 bucket to serve your static content then you can’t do it using the base64 string.
  3. You have to store the base64 encoded string in your database & always have to serve your images from the main server & you can’t use the power of CDN.
  4. Image caching becomes a problem when you use base64.
  5. The API response payload size will increase dramatically depending upon the image size.

These are the major points you should consider while storing/transferring base64 encoded images over a network. Still, There can be some cases where it can be useful depending upon your application’s architecture.

Prerequisite.

To follow along with the rial you need to familiar with the Flutter framework(of course you are, hence you are here) and the http package provided by the Flutter team to perform any network-related tasks.

For the backend, I am going to use PHP to handle the incoming request. You can choose whatever language or framework you like as it is completely language/framework agnostic.

Installations.

Open the terminal in the directory where you want to store your flutter application project & run the following command to create a fresh project of flutter.

flutter create <YOUR_PROJECT_NAME>

This command will create a flutter project for you. Open this project in either VS Code or Android Studio whichever you like. For this tutorial, I am going to use the VS Code.

Now we need to install some third-party packages to ease our development efforts. To install the dependencies open your pubspec.yaml file & add the following dependencies under the dependencies,

  • http – This is the main package which will do all the heavy lifting for any network-related tasks.
  • image_picker: 0.4.12+1 – This will help us to pick any images from our device’s gallery or it can also capture the image from the camera itself.
  • mime: ^0.9.6+3 – This package provides some utility methods to detect the MIME type of any files.
  • toast: ^0.1.5 – To show some toast messages in our application.

Now it should look like this in your file,

Required dependencies

Required dependencies

Step 1: Creating The Basic Layout.

Let’s first implement the basic design of our app. In this layout, the user can see the button to pick an image. I will use the bottom sheet to display two options(Camera & Gallery) for users to opt for. After picking the image user can see the preview of the image before uploading & a button to upload the image.

main.dart

import 'package:flutter/material.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'dart:convert';
import 'package:http_parser/http_parser.dart';
import 'package:toast/toast.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Image Upload Demo',
        theme: ThemeData(primarySwatch: Colors.pink),
        home: ImageInput());
  }
}

class ImageInput extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageInput();
  }
}

class _ImageInput extends State<ImageInput> {
  // To store the file provided by the image_picker
  File _imageFile;

  // To track the file uploading state
  bool _isUploading = false;

  void _openImagePickerModal(BuildContext context) {
    final flatButtonColor = Theme.of(context).primaryColor;
    print('Image Picker Modal Called');
    showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return Container(
            height: 150.0,
            padding: EdgeInsets.all(10.0),
            child: Column(
              children: <Widget>[
                Text(
                  'Pick an image',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                SizedBox(
                  height: 10.0,
                ),
                FlatButton(
                  textColor: flatButtonColor,
                  child: Text('Use Camera'),
                  onPressed: () {
                    
                  },
                ),
                FlatButton(
                  textColor: flatButtonColor,
                  child: Text('Use Gallery'),
                  onPressed: () {
                    
                  },
                ),
              ],
            ),
          );
        });
  }

  
  Widget _buildUploadBtn() {
    Widget btnWidget = Container();

    if (_isUploading) {
      // File is being uploaded then show a progress indicator
      btnWidget = Container(
          margin: EdgeInsets.only(top: 10.0),
          child: CircularProgressIndicator());
    } else if (!_isUploading && _imageFile != null) {
      // If image is picked by the user then show a upload btn

      btnWidget = Container(
        margin: EdgeInsets.only(top: 10.0),
        child: RaisedButton(
          child: Text('Upload'),
          onPressed: () {
            _startUploading();
          },
          color: Colors.pinkAccent,
          textColor: Colors.white,
        ),
      );
    }

    return btnWidget;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Upload Demo'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(top: 40.0, left: 10.0, right: 10.0),
            child: OutlineButton(
              onPressed: () => _openImagePickerModal(context),
              borderSide:
                  BorderSide(color: Theme.of(context).accentColor, width: 1.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.camera_alt),
                  SizedBox(
                    width: 5.0,
                  ),
                  Text('Add Image'),
                ],
              ),
            ),
          ),
          _imageFile == null
              ? Text('Please pick an image')
              : Image.file(
                  _imageFile,
                  fit: BoxFit.cover,
                  height: 300.0,
                  alignment: Alignment.topCenter,
                  width: MediaQuery.of(context).size.width,
                ),
          _buildUploadBtn(),
        ],
      ),
    );
  }
}

Our layout will look like this,

Layout Design

Layout Design

When you click on the button to add image our bottom sheet modal will look like this,

 

Bottom sheet layout

Bottom sheet layout

Step 2: Adding the Image Picking Feature.

The image_picker package we have installed provides an easy way to get the image from either the camera or the gallery. Let’s create a method for that.

void _getImage(BuildContext context, ImageSource source) async {
    File image = await ImagePicker.pickImage(source: source);

    setState(() {
      _imageFile = image;
    });

    // Closes the bottom sheet
    Navigator.pop(context);
  }

The ImagePicker.pickImage() is asynchronous by nature, So we need to use async/await for this method. After selecting the file this method returns the File object which we are storing in our state. Don’t forget to call this method inside the onPressed method of buttons in the bottom sheet like this,

onPressed: () {
  _getImage(context, ImageSource.camera);
}

After picking the image, the image preview will be shown like this,

Image preview widget

Image preview widget

Step 3: Backend Setup To Handle The Image Upload Process.

Now before moving further in our flutter app, I want to set up the backend/API code to handle the file upload process So that we can have an endpoint exposed to send our request.

I have created a folder named flutterdemoapi in my C:/xampp/htdocs folder. Inside that, I have created a file called api.php in which I will write all the logic for file uploading.

For this, I am going to use this bare minimum PHP code to handle the file upload.

<?php
if(isset($_FILES["image"]["name"])) {
  
    // Make sure you have created this directory already
    $target_dir = "uploads/";
  
    // Generate a random name 
    $target_file = $target_dir . md5(time()) . '.' . $_POST['ext'];

    $check = getimagesize($_FILES["image"]["tmp_name"]);
    if($check !== false) {
        if (move_uploaded_file($_FILES["image"]["tmp_name"], $target_file)) {
      echo json_encode(['response' => "The image has been uploaded."]);
     	}else {
      echo json_encode(["error" => "Sorry, there was an error uploading your file."]); 
    }
    } else {
        echo json_encode(["error" => "File is not an image."]);
       
    }
}

 else {
     echo json_encode(["error" => "Please provide a image to upload"]);
}
?>

Step 4: Implementing The Image Upload Feature.

I am going to create two methods to implement this feature. The _uploadImage() method will form our multipart request & will send the request to the server. The _startUploading() will be called after the upload button is pressed & will do some post uploading process.

But we have to define our URL endpoint to send the request to. If you are testing it on your local machine like me then you just can’t point to http://localhost or http://127.0.0.1 because it will not work. You need to get the IPv4 address of your machine to test it locally.

To get the IPv4 address of your machine on windows pc, you can open your cmd & fire ipconfig command.

ipv4 address command

ipv4 address command

 

Here is the full code of implementation,

import 'package:flutter/material.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'dart:convert';
import 'package:http_parser/http_parser.dart';
import 'package:toast/toast.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Image Upload Demo',
        theme: ThemeData(primarySwatch: Colors.pink),
        home: ImageInput());
  }
}

class ImageInput extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageInput();
  }
}

class _ImageInput extends State<ImageInput> {
  // To store the file provided by the image_picker
  File _imageFile;

  // To track the file uploading state
  bool _isUploading = false;

  String baseUrl = 'http://YOUR_IPV4_ADDRESS/flutterdemoapi/api.php';

  void _getImage(BuildContext context, ImageSource source) async {
    File image = await ImagePicker.pickImage(source: source);

    setState(() {
      _imageFile = image;
    });

    // Closes the bottom sheet
    Navigator.pop(context);
  }

  Future<Map<String, dynamic>> _uploadImage(File image) async {
    setState(() {
      _isUploading = true;
    });

    // Find the mime type of the selected file by looking at the header bytes of the file
    final mimeTypeData =
        lookupMimeType(image.path, headerBytes: [0xFF, 0xD8]).split('/');

    // Intilize the multipart request
    final imageUploadRequest =
        http.MultipartRequest('POST', Uri.parse(baseUrl));

    // Attach the file in the request
    final file = await http.MultipartFile.fromPath('image', image.path,
        contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));

    // Explicitly pass the extension of the image with request body
    // Since image_picker has some bugs due which it mixes up
    // image extension with file name like this filenamejpge
    // Which creates some problem at the server side to manage
    // or verify the file extension
    imageUploadRequest.fields['ext'] = mimeTypeData[1];

    imageUploadRequest.files.add(file);

    try {
      final streamedResponse = await imageUploadRequest.send();

      final response = await http.Response.fromStream(streamedResponse);

      if (response.statusCode != 200) {
        return null;
      }

      final Map<String, dynamic> responseData = json.decode(response.body);

      _resetState();

      return responseData;
    } catch (e) {
      print(e);
      return null;
    }
  }

  void _startUploading() async {
    final Map<String, dynamic> response = await _uploadImage(_imageFile);
    print(response);
    // Check if any error occured
    if (response == null || response.containsKey("error")) {
      Toast.show("Image Upload Failed!!!", context,
          duration: Toast.LENGTH_LONG, gravity: Toast.BOTTOM);
    } else {
      Toast.show("Image Uploaded Successfully!!!", context,
          duration: Toast.LENGTH_LONG, gravity: Toast.BOTTOM);
    }
  }

  void _resetState() {
    setState(() {
      _isUploading = false;
      _imageFile = null;
    });
  }

  void _openImagePickerModal(BuildContext context) {
    final flatButtonColor = Theme.of(context).primaryColor;
    print('Image Picker Modal Called');
    showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return Container(
            height: 150.0,
            padding: EdgeInsets.all(10.0),
            child: Column(
              children: <Widget>[
                Text(
                  'Pick an image',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                SizedBox(
                  height: 10.0,
                ),
                FlatButton(
                  textColor: flatButtonColor,
                  child: Text('Use Camera'),
                  onPressed: () {
                    _getImage(context, ImageSource.camera);
                  },
                ),
                FlatButton(
                  textColor: flatButtonColor,
                  child: Text('Use Gallery'),
                  onPressed: () {
                    _getImage(context, ImageSource.gallery);
                  },
                ),
              ],
            ),
          );
        });
  }

  Widget _buildUploadBtn() {
    Widget btnWidget = Container();

    if (_isUploading) {
      // File is being uploaded then show a progress indicator
      btnWidget = Container(
          margin: EdgeInsets.only(top: 10.0),
          child: CircularProgressIndicator());
    } else if (!_isUploading && _imageFile != null) {
      // If image is picked by the user then show a upload btn

      btnWidget = Container(
        margin: EdgeInsets.only(top: 10.0),
        child: RaisedButton(
          child: Text('Upload'),
          onPressed: () {
            _startUploading();
          },
          color: Colors.pinkAccent,
          textColor: Colors.white,
        ),
      );
    }

    return btnWidget;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Upload Demo'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(top: 40.0, left: 10.0, right: 10.0),
            child: OutlineButton(
              onPressed: () => _openImagePickerModal(context),
              borderSide:
                  BorderSide(color: Theme.of(context).accentColor, width: 1.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.camera_alt),
                  SizedBox(
                    width: 5.0,
                  ),
                  Text('Add Image'),
                ],
              ),
            ),
          ),
          _imageFile == null
              ? Text('Please pick an image')
              : Image.file(
                  _imageFile,
                  fit: BoxFit.cover,
                  height: 300.0,
                  alignment: Alignment.topCenter,
                  width: MediaQuery.of(context).size.width,
                ),
          _buildUploadBtn(),
        ],
      ),
    );
  }
}

Conclusion.

You have learned that it is fairly easy to upload images to any REST API in Flutter using the http package. Now you don’t need to convert your images to base64 to upload it on your server. You have seen what are the cons of using the base64 method. The way I have shown you to upload images is also the same for uploading any files using the http package.

If you have any suggestions or if you face any problem please write it down in the comment box…

Happy Coding…:)

 

Ropali Munshi
Ropali Munshi
Ropali Munshi is fullstack PHP Developer. He is passionate developer who loves to learn and expirement with new programming languages , libraries and frameworks. Nowdays he is more into the JavaScript realm.

7 Comments

  1. kanapong says:

    do you have a upload source code to github can i get it
    thank you

  2. Azam says:

    Sory..where the link source code

  3. Abdullah says:

    I/flutter (20210): FormatException: Unexpected character (at character 1)
    I/flutter (20210):
    I/flutter (20210): ^
    I/flutter (20210): null

    why this error ???

  4. Brian says:

    I can see that this uploads only one image at a time. Why not help us upload all images at once? May be 5 images or more to the server side.

    Regards

Leave a Reply

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