Flutter file_picker and Uploading files to Amazon S3 with AWS Amplify

Flutter file_picker and Uploading files to Amazon S3 with AWS Amplify

#17DaysOfFlutter

File picking is one of the most important features that your application can have. You can use it for image or any kind of file uploading mechanism.

For uploading files, there are many alternatives. One can use their own service as well as other helpful services like AWS Amplify. In this video you will learn about using native file picker and uploading those files to cloud by using AWS Amplify.

Requirements:

  • Flutter 3.0 or higher

  • Amplify CLI with an AWS account

    In case you have not created an AWS account before, you can follow this guide that I created.

    Be sure to also do any platform specific setup for AWS Amplify from the visual guide here.

Using file_picker

If your application is only relying on picking images, you can use image_picker. However, the image picker only supports iOS, Android and Web. But there is another alternative, you can use. It is a library called file_picker. A package that allows you to use the native file explorer to pick single or multiple files, with extensions filtering support.

Creating a new Flutter Project

You will create a new project with the flutter create command.

flutter create file_upload_example --org dev.salih

After Flutter 3.0, this will create a new project with all platforms.

Now that you created a project, open the project with your preferred IDE.

Adding file_picker to the project

After file_picker 4.0, the library made our lives way more easier. For adding the file_picking capabilities, you need to add the following to pubspec.yaml file.

dependencies:
  file_picker: ^5.2.5

After that run flutter pub get.

Adding file picking mechanism

Go to lib/main.dart and remove everything in the code. Afterwards, add the following.

import 'package:flutter/material.dart';

void main() {
  runApp(const FilePickingApp());
}

class FilePickingApp extends StatelessWidget {
  const FilePickingApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('File Picking')),
        body: const FilePickingView(),
      ),
    );
  }
}

class FilePickingView extends StatefulWidget {
  const FilePickingView({Key? key}) : super(key: key);

  @override
  State<FilePickingView> createState() => _FilePickingViewState();
}

class _FilePickingViewState extends State<FilePickingView> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

Now it is time to add the UI to FilePickingView. Update the UI with the following:


class _FilePickingViewState extends State<FilePickingView> {
  bool _isFileSelected = false;

  void _selectFile() {
    setState(() {
      _isFileSelected = true;
    });
  }

  void _deselectFile() {
    setState(() {
      _isFileSelected = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            _isFileSelected
                ? 'You selected a file. Click the button below to remove file'
                : 'Click the button below to select a file.',
          ),
          ElevatedButton(
            onPressed: _isFileSelected ? _deselectFile : _selectFile,
            child: Text(isFileSelected ? 'Deselect File' : 'Select File'),
          )
        ],
      ),
    );
  }
}

Now if you run the application, it should look like the following:

Now that you have the basic app structure, let's continue with file picking mechanism.

Go to the _selectFile function and update it as follows:

Future<void> _selectFile() async {
  final result = await FilePicker.platform.pickFiles();
  if (result != null) {
    final file = result.files.single;
    setState(() {
      _isFileSelected = true;
    });
  }
}

If you go to and run the application. You can see that the application is opening a native file picker on all the platforms.

Alright, now it is time to add AWS Amplify to your project for cloud upload mechanism.

Adding AWS Amplify for Uploading Files to AWS S3

AWS Amplify is the set of tools that AWS provides you to make your life easier by helping Frontend Devs to become full-stack devs. AWS Amplify supports many platforms and one of them is Flutter. In case you are wondering about AWS Amplify, you can check out the video that I created a few days back.

Initializing AWS Amplify with Flutter Application

Open up your terminal and direct yourself to the root directory of your application. Initialize AWS Amplify in your Flutter application by writng amplify init:

(1)
msalihg@ file_upload_example % amplify init
Note: It is recommended to run this command from the root of your app directory
(2)
? Enter a name for the project fileuploadexample
The following configuration will be applied:
(3)
Project information
| Name: fileuploadexample
| Environment: dev
| Default editor: Visual Studio Code
| App type: flutter
| Configuration file location: ./lib/

? Initialize the project with the above configuration? Yes
Using default provider  awscloudformation
(4)
? Select the authentication method you want to use: AWS profile

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use salihgueler-frankfurt
Adding backend environment dev to AWS Amplify app: <project-id>

Let's check out the flow in detail:

  • (1): amplify init is called to initialize the AWS Amplify in our app.

  • (2): You can either accept the default value that is shown in parenthesis.

  • (3): Amplify defines your project structure, you can accept themor configure them.

  • (4): Select AWS profile and pick the AWS account that you created earlier.

Now let's add the file upload mechanism with AWS Amplify.

Be sure to add any platform specific setup to your project from here --> https://docs.amplify.aws/lib/project-setup/platform-setup/q/platform/flutter/

Add AWS Amplify Storage for Uploading Files

For adding Amplify storage, you need to write amplify add storage to your terminal:

(1)
msalihg file_upload_example % amplify add storage
(2)
? Select from one of the below mentioned services: Content (Images, audio, video, etc.)
(3)
✔ You need to add auth (Amazon Cognito) to your project in order to add storage for user files. Do you want to add auth now? (Y/n) · yes

Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito. 
 (4)
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
✅ Successfully added auth resource <project-id> locally
(5)
✔ Provide a friendly name for your resource that will be used to label this category in the project: · <resouce-id>
(6)
✔ Provide bucket name: · <bucket-name>
(7)
✔ Who should have access: · Auth users only
(8)
✔ What kind of access do you want for Authenticated users? · create/update, read, delete
(9)
✔ Do you want to add a Lambda Trigger for your S3 Bucket? (y/N) · no

Now let's examine the steps that we have taken above:

  • (1): Write amplify add storage to start the process.

  • (2): Be sure to select Content (Images, audio, video, etc.).

  • (3): For using S3 buckets, you need to do an auth either with regular accounts or regular + guest accounts.

  • (4): Pick the default auth configuration with username login.

  • (5): Once you add the authentication add a default name for your resources or select the default one.

  • (6): Select a bucket name or select the default one.

  • (7): Select the people who can upload files. It can be either authenticated users or auth +guest users. I picked the Auth users

  • (8): Select create/update, read, delete by using arrows and space

  • (9): No need for a Lambda trigger for now.

Lastly, write amplify push to create these resources on the cloud. Next stage is adding authentication flow.

Adding Authenticator UI

Amplify Authenticator UI is the UI library that AWS provides you to make your authentication flow implementation better.

For using it add the following to pubspec.yaml:

dependencies:
  amplify_flutter: ^1.0.0-0
  amplify_auth_cognito: ^1.0.0-0
  amplify_authenticator: ^1.0.0-0

Afterwards, update the main function like the following by also adding _configureAmplify function:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await _configureAmplify();
  runApp(const FilePickingApp());
}

Future<void> _configureAmplify() async {
  try {
    await Amplify.addPlugin(AmplifyAuthCognito());
    await Amplify.configure(amplifyconfig);
    safePrint('Successfully configured');
  } on Exception catch (e) {
    safePrint('Error configuring Amplify: $e');
  }
}

This way, you have ensured that Amplify library that you will be using is configured before the app started.

Lastly update the build function in the FilePickingApp class:

@override
Widget build(BuildContext context) {
  return Authenticator(
    child: MaterialApp(
      builder: Authenticator.builder(),
      home: Scaffold(
        appBar: AppBar(title: const Text('File Picking')),
        body: const FilePickingView(),
      ),
    ),
  );
}

Now you are ready to implement file upload mechanism.

Uploading files

Before you upload the first file, let's update the class a bit:


class _FilePickingViewState extends State<FilePickingView> {
  PlatformFile? _selectedFile;

  Future<void> _selectFile() async {
    final result = await FilePicker.platform.pickFiles();

    if (result != null) {
      setState(() {
        _selectedFile = result.files.single;
      });
    }
  }

  void _deselectFile() {
    setState(() {
      _selectedFile = null;
    });
  }

  @override
  Widget build(BuildContext context) {
    final isFileSelected = _selectedFile != null;
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            isFileSelected
                ? 'You selected a file at path ${_selectedFile?.path}'
                : 'Click the button below to select a file.',
          ),
          ElevatedButton(
            onPressed: isFileSelected ? _deselectFile : _selectFile,
            child: Text(isFileSelected ? 'Select File' : 'Upload File'),
          )
        ],
      ),
    );
  }
}

This has a reference to the file to be uploaded now. Let's update the file. First add the libraries to the pubspec.yaml file:

dependencies:
  amplify_storage_s3: ^1.0.0-0

Afterwards, update the _configureAmplify function from the main.dart:

Future<void> _configureAmplify() async {
  try {
    await Amplify.addPlugin(AmplifyStorageS3());
    await Amplify.addPlugin(AmplifyAuthCognito());
    await Amplify.configure(amplifyconfig);
    safePrint('Successfully configured');
  } on Exception catch (e) {
    safePrint('Error configuring Amplify: $e');
  }
}

Lastly, update the _selectFile and add the _uploadExampleData function to upload the file and add a value representation to the uploaded file in _FilePickingViewState class:

// (1)
double _uploadedPercentage = 0;

Future<void> _selectFile() async {
  final result = await FilePicker.platform.pickFiles();
  if (result != null) {
    // (2)
    uploadExampleData(result.files.first);
  }
}

Future<void> uploadExampleData(PlatformFile file) async {
  try {
    setState(() {
      _selectedFile = file;
    });
    final result = await Amplify.Storage.uploadFile(
      // (3)
      localFile: AWSFile.fromPath(
        file.path!,
      ),
      key: file.name,
      onProgress: (progress) {
        // (4)
        setState(() {
          _uploadedPercentage = progress.fractionCompleted;
        });
      },
    ).result;
    safePrint('Uploaded data to location: ${result.uploadedItem.key}');
    _deselectFile();
  } on StorageException catch (e) {
    safePrint(e.message);
  }
}
  • (1): This is the value to keep the upload percentage

  • (2): If the file is selected, the upload function will be called

  • (3): AWSFile is a special file format to create from the path

  • (4): Each percentage change is shown with the fraction completed

and update the build function:

@override
Widget build(BuildContext context) {
  final isFileSelected = _selectedFile != null;
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          isFileSelected
              ? 'You selected a file at path ${_selectedFile?.path}'
              : 'Click the button below to select a file.',
        ),
        if (isFileSelected)
          CircularProgressIndicator(
            value: _uploadedPercentage,
          ),
        ElevatedButton(
          onPressed: isFileSelected ? _deselectFile : _selectFile,
          child: Text(isFileSelected ? 'Deselect File' : 'Select File'),
        )
      ],
    ),
  );
}

Now if you run your application, you can see that the app will show a login flow and uploading mechanism for your files.

BONUS: Listing Files

We can have all the uploads on the world, if we do not show it, that won’t matter. In this section, you will add a new toolbar icon to list the uploaded files.

First upload the FilePickingApp class with new icon buttons:

class FilePickingApp extends StatelessWidget {
  const FilePickingApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Authenticator(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        builder: Authenticator.builder(),
        home: Builder(builder: (context) {
          return Scaffold(
            appBar: AppBar(
              title: const Text('File Picking'),
              actions: [
                IconButton(
                  onPressed: () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) {
                          return const ListPreviousFilesView();
                        },
                      ),
                    );
                  },
                  icon: const Icon(Icons.list_alt),
                ),
                IconButton(
                  onPressed: () {
                    Amplify.Auth.signOut();
                  },
                  icon: const Icon(Icons.exit_to_app),
                ),
              ],
            ),
            body: const FilePickingView(),
          );
        }),
      ),
    );
  }
}

Now you will see ListPreviousFilesView is giving you an error. The reason is that you have not added that UI yet. Add the ListPreviousFilesView widget as below:

class ListPreviousFilesView extends StatefulWidget {
  const ListPreviousFilesView({Key? key}) : super(key: key);

  @override
  State<ListPreviousFilesView> createState() => _ListPreviousFilesViewState();
}

class _ListPreviousFilesViewState extends State<ListPreviousFilesView> {
  final items = <StorageItem>[];

  Future<void> listAlbum() async {
    try {
      // (1)  
      final result = await Amplify.Storage.list().result;
      // (2)
      items
        ..clear()
        ..addAll(result.items);

      safePrint('Listed items: ${result.items}');
    } on StorageException catch (e) {
      safePrint(e.message);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Previous Uploads')),
      // (3)
      body: FutureBuilder<void>(
        future: listAlbum(),
        builder: ((context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else {
            // (4)
            return ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(items[index].key));
              },
            );
          }
        }),
      ),
    );
  }
}
  • (1): You are listing all the files. You can see how you can reach the guest files as well with the guide here.

  • (2): Retrieved items are added to the list by removing the previous ones.

  • (3): Calls the async function with the FutureBuilder

  • (4): Fetched files are shown as list items

Wrapping up

If you run the application right now, you can see the behavior in the video below:

You can use AWS Amplify to use AWS resources with the other technologies. You can see that it also makes your life easier and you get feedback on each file update with each fraction change.

If you have any questions or any feedback you can reach out to me over Twitter and see the project over GitHub.