Making a Google Drive Clone | Flutter and AWS
Tags: flutter, aws, amplify
Published: 2020-09-30 20:09:59




Let’s make a file-sharing application — affectionately named Doogle Grive — to show off what we can accomplish with Flutter and Amplify.

**NOTE: Amplify Flutter is still in developer preview and is not recommended for production use at this time. **

![](https://cdn-images-1.medium.com/max/2560/1*YJpnBniFLJagJ_6hmbcMTQ.png)

## Introducing Doogle Grive

![Demonstration of the App](https://cdn-images-1.medium.com/max/2000/1*qUk7zWre9vhf2uZaU1i32w.gif)*Demonstration of the App*
> Amplify is great, you won’t be 4 nested maps deep in a JSON config trying to bit twiddle your way to a working full-stack application.

## Some Background

### What is Amplify?

Put simply, [Amplify](https://aws.amazon.com/amplify/) is a toolset that makes it easy (as in, one-click easy) to set up an AWS backend for your mobile application. That might sound a little too good to be true. Honestly, you still need to write *some* code, but at least you won’t be 4 nested maps deep in a JSON config trying to bit twiddle your way to a working full-stack application.

### Why should I care?

Up until now, Flutter developers have been somewhat restricted to the Google ecosystem of backend resources. This has some pitfalls, particularly for people who are AWS aficionados or already have AWS deployed systems that they want to be integrated with a Flutter app.

With AWS officially supporting Amplify for Flutter, there is only room for growth in the *Futter w/ AWS *universe, so learning the fundamentals has some serious upside potential.

Enough talk, let’s get into it.

## Setting up Amplify

For super-detailed installation instructions, check out [this article](https://medium.com/@vennify.education/flutter-apps-with-aws-backend-part-1-basic-setup-7e1dd53e9161) I wrote on the topic. I’ll give you speedier instructions in this article, don’t blink or you might miss it.

* **Create an AWS account**

Head over to the AWS [portal](https://aws.amazon.com/account/) and create an account under the ‘free’ plan.

![](https://cdn-images-1.medium.com/max/2000/1*jQdoug0RyyEN3cC68IUthg.png)

* **Install the Amplify CLI**

You will need [flutter](https://flutter.dev/docs/get-started/install), [node.js](https://nodejs.org/en/), [npm](https://www.npmjs.com/), and [git](https://git-scm.com/), then it is as simple as:

    npm install -g @aws-amplify/cli@flutter-preview

* **Initialize your Flutter project**

In the root directory of your flutter project, run:

amplify init 

This will guide you through the setup process for your application. Once that is complete, run:

amplify add auth

![](https://cdn-images-1.medium.com/max/2000/1*KHiWCRrd9OYYsmcNVmK_Wg.png)

amplify add analytics

![](https://cdn-images-1.medium.com/max/2626/1*FRmlC3SPY-y4Wj9pHd68OQ.png)

amplify add storage

![](https://cdn-images-1.medium.com/max/2000/1*hrq8gKVv_6I8eLzLghSC1g.png)



* **Configuration Code**

Our application contains some code that adds the AWS plugins we want to use. In this case, we need Authentication, Analytics, and of course, Storage. All powered by our omnipresent pal in the sky — AWS. 

```dart
  void configureAmplify() async {
    if (!mounted) return;

    try {
      AmplifyAnalyticsPinpoint analyticsPlugin = AmplifyAnalyticsPinpoint();
      AmplifyAuthCognito authPlugin = AmplifyAuthCognito();
      AmplifyStorageS3 storage = AmplifyStorageS3();

      // Authentication -> AWS Cognito
      // Analytics -> AWS Pinpoint
      // Storage -> AWS S3
      amplifyInstance.addPlugin(
          authPlugins: [authPlugin],
          analyticsPlugins: [analyticsPlugin],
          storagePlugins: [storage]);

      await amplifyInstance.configure(amplifyconfig);
    } catch (e) {
      print(e.toString());
    }
  }
```

If any of that confused you or gave you trouble, check out this [video ](https://www.youtube.com/watch?v=70mFj8FZpTI&t=2s)or this [article](https://medium.com/@vennify.education/flutter-apps-with-aws-backend-part-1-basic-setup-7e1dd53e9161) (also made by me), or the official [docs](https://docs.amplify.aws/start/getting-started/setup/q/integration/flutter)

## Introducing the UI

Time to crack the knuckles and start coding. If you want to follow along, ‘checkout’ [**this commit](https://github.com/Vennify-Inc/DoogleGrive/tree/26e9111b218a1cc20e3a14405f5643268045578b) **in the repo.

This app is designed to make the AWS logic as clear as possible — so it is minimal in its design.

### Login and Signup — using open-source for all its worth

The Login UI is built using the [flutter_login ](https://pub.dev/packages/flutter_login)package, which makes it super trivial to make an interactive user interface.

![](https://cdn-images-1.medium.com/max/2000/1*9ixg2YgBlYBOazPIFVYz2g.gif)

Not bad eh? All we have to do is specify the callbacks to achieve the functionality we want. We will do this in the next part though.

```dart

FlutterLogin(
              logo: 'images/trash_can.png',
              onLogin: // SIGN IN FUNCTION,
              onSignup: // REGISTER FUNCTION ,
              onRecoverPassword: (_) => null,
              onSubmitAnimationCompleted: // PORTAL FUNCTION,
              title: 'Doogle Grive')
```

### Confirmation

psst…you can probably skip this section, its just a simple input form to submit the confirmation code. 

Still here? Fine, I will show you the confirmation UI and its code.

![Super-Confirmation Form](https://cdn-images-1.medium.com/max/2000/1*ntBbyr6cNVg-CPpKbB4sQQ.png)*Super-Confirmation Form*

```dart

Center(
            child: Column(
          children: [
            SizedBox(
              height: 50,
            ),
            Padding(
              padding: const EdgeInsets.all(20.0),
              child: TextField(
                onChanged: (text) {
                  code = text;
                },
                decoration: InputDecoration(
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(20),
                      borderSide: BorderSide(color: Colors.teal),
                    ),
                    hintText: 'Enter your confirm code here...'),
              ),
            ),
            RaisedButton(onPressed: confirmUser, child: Text("Submit Code")),
            RaisedButton(
              onPressed: () {
                Navigator.pushReplacement(
                    context, MaterialPageRoute(builder: (_) => Login()));
              },
              child: Text("Go Back"),
            ),
          ],
        )),
```

AWS Cognito will send a 6 digit confirmation code to the signup email, and upon entering this code the user is granted access to their account.

### Bucket Viewer™

The BucketViewer™ UI is all about displaying the cloud files that belong to the current user. 

We will render each file on the UI as a Card, the end result looks something like this:

![](https://cdn-images-1.medium.com/max/2000/1*lanZvYJyBw1EuleEQxOUvA.png)

We have an icon representing the file type, the name of the file, and  IconButtons for downloading and deleting the file.

```dart
  Widget fileViewer(StorageItem file) {
    return Card(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Expanded(
              flex: 3,
              child: Row(
                children: [
                  getFileIcon(file.key.split('/').last),
                  Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Text(file.key.split('/')[1]),
                  )
                ],
              )),
          Expanded(
            flex: 1,
            child: Row(
              children: [
                IconButton(
                  icon: Icon(Icons.cloud_download),
                  onPressed: () {downloadFile(file);},
                ),
                IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () {deleteFile(file);},
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
```

**TLDR**: The widget is constructed using the cloud data (of typeStorageItem ), and the buttons trigger some more of our cloud functions called downloadFile anddeleteFile . Don’t worry, we will cover these soon.

Each of our files is displayed in a [ListView](https://api.flutter.dev/flutter/widgets/ListView-class.html) widget, which gives us the scroll-ability we desire. The ListView widget is rendered by a [FutureBuilder](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html), which waits on the result of one of our trusty AWS API calls. Take a look at the final result and the UI code:

![Scrollable File List](https://cdn-images-1.medium.com/max/2000/1*Sl-j-1KXARTrrbO01AJySA.gif)*Scrollable File List*

```dart

FutureBuilder(
              future: listFiles(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return ListView.builder(
                    physics: AlwaysScrollableScrollPhysics(),
                    scrollDirection: Axis.vertical,
                    shrinkWrap: true,
                    itemBuilder: (BuildContext context, int index) {
                      return fileViewer(snapshot.data[index]);
                    },
                    itemCount: snapshot.data.length,
                  );
                } else {
                  return ListView();
                }
              },
            )
```

So our yet-to-be-revealed listFiles function creates a [Future](https://api.flutter.dev/flutter/dart-async/Future-class.html) containing a List<StorageItem> , which in turn gets converted into a UI object when the Future is ready. When the UI is still waiting on the result of our function, an empty list is displayed.

One last thing, a handy logout button in the top right corner to log our user out. 

![Logout Button](https://cdn-images-1.medium.com/max/2000/1*0amSaG9MBufdqiKqpMN9lg.png)*Logout Button*

Okay okay okay, I know what you are thinking…you came here to learn about AWS and I keep showing you basic Flutter UI code. Let's get to the good stuff.

## Powering our UI with AWS

### Authentication Functions featuring AWS Cognito

Our authentication service is provisioned using [AWS Cognito](https://aws.amazon.com/blogs/mobile/announcing-aws-amplify-flutter-developer-preview/), which is accessible via calls to Amplify.Auth.methodName . Let’s walk through the functions that power our authentication.

### Sign Up

This is the first function used by a prospective user. We sign up using the name (in our case an email) and password submitted in our login form. It is simple, and it is supposed to be.

```dart

  Future<String> _registerUser(LoginData data) async {
    try {
      Map<String, dynamic> userAttributes = {
        "email": data.name,
      };
      SignUpResult res = await Amplify.Auth.signUp(
          username: data.name,
          password: data.password,
          options: CognitoSignUpOptions(userAttributes: userAttributes));

      _userName = data.name;

      _isSignedUp = res.isSignUpComplete;

      return null;
    } on AuthError catch (e) {
      print(e);
      return "Register Error: " + e.toString();
    }
  }
```

After signing up our first use, run amplify auth console to explore our Cognito managed user pool. 

![An unconfirmed user added to the user pool](https://cdn-images-1.medium.com/max/2000/1*sIvvPGIScCyMMiQvhJOSFw.png)*An unconfirmed user added to the user pool*

We still need to confirm our user somehow, which segues us into…

### Confirm

Pretty self-explanatory on this one. A confirmation code is sent to our user’s signup email and they enter it to activate their account.

```dart
  confirmUser() async {
    SignUpResult res = await Amplify.Auth.confirmSignUp(
        username: this.widget.username,
        confirmationCode: code.trim()
    );
  }
```

![Confirming our code](https://cdn-images-1.medium.com/max/2000/1*c7ob7jw_t4qIfyXZXwxf5A.gif)*Confirming our code*

In your Cognito console, your user should now have the status of CONFIRMED.

### **Sign In**

This is more or less the same as the signup. Note how we are keeping track of the results of our API calls using _isSignedIn and _isSignedUp to eventually decide what screen to send our user to after they are finished with our login page. I don’t show this function in this article, but check out the [repo](https://github.com/Vennify-Inc/DoogleGrive/) if you are curious.

```dart

  Future<String> _signIn(LoginData data) async {
    try {
      SignInResult res = await Amplify.Auth.signIn(
        username: data.name,
        password: data.password,
      );

      _isSignedin = res.isSignedIn;
    } on AuthError catch (e) {
      print(e);
      Alert(context: context, type: AlertType.error, title: "Login Failed")
          .show();
      return 'Log In Error: ' + e.toString();
    }
  }
```

### Logout

We log our user out and then issue a call to pushReplacement to pop the current page of the stack and replace it with the **Login **page.

```dart

  logout() async {
    try {
      Amplify.Auth.signOut();

      Navigator.pushReplacement(
          context, MaterialPageRoute(builder: (context) => Login()));
    } on AuthError catch (e) {
      Alert(
          context: context,
          type: AlertType.error,
          desc: "Error Logging Out: " + e.toString());
    }
  }
```

### Upload File

There are a few important things that happen here. 

First, we get the user that is currently logged in using the auth package in the call to getCurrentUser()**. **We use this username along with the filename to create a unique key for our file, then we upload it. 

setState()and the uploading flag allow us to change the appearance of the upload-file button while our file is uploading. A small detail but a nice touch for our application. 

Finally, we submit an analytics event to [Amazon Pinpoint](https://aws.amazon.com/pinpoint/) that tracks the number of bytes a user has uploaded.

```dart

  void uploadFile() async {
    File file = await FilePicker.getFile();
    AuthUser user = await Amplify.Auth.getCurrentUser();

    try {
      if (file.existsSync()) {
        setState(() {
          uploading = true;
        });

        final key = user.username + '/' + file.path.split('/').last;

        await Amplify.Storage.uploadFile(key: key, local: file);
        
        // log an upload event, tracking the amount of bytes uploaded
        AnalyticsEvent event = AnalyticsEvent("file_upload_event");

        event.properties.addStringProperty("user", user.username);
        event.properties.addIntProperty("file_size", file.lengthSync());

        Amplify.Analytics.recordEvent(event: event);
        Amplify.Analytics.flushEvents();

        setState(() {
          uploading = false;
        });
      }
    } catch (e) {
      Alert(
          context: context,
          type: AlertType.error,
          desc: "Error Uploading File: " + e.toString());
    }
  }
```

### Download File

First, we use [DownloadsPathProvider ](https://pub.dev/packages/downloads_path_provider)to get the target directory for our download. 

getUrl fetches the download URL for our file, which will expire after 1 hour because of the expires option in GetUrlOptions.

The calls to checkPermission and FlutterDownloader check the app’s download permissions and then downloads the file. For more details, check out the [FlutterDownloader ](https://pub.dev/packages/flutter_downloader)documentation or the GitHub [repo ](https://github.com/Vennify-Inc/DoogleGrive/)for the full codebase.

```dart
  void downloadFile(StorageItem item) async {
    try {
      var dir = await DownloadsPathProvider.downloadsDirectory;
      var url = await Amplify.Storage.getUrl(
          key: item.key, options: GetUrlOptions(expires: 3600));

      await checkPermission();

      await FlutterDownloader.enqueue(
        url: url.url,
        fileName: item.key.split('/').last,
        savedDir: dir.path,
        showNotification: true,
        openFileFromNotification: true,
      );
    } catch (e) {
      print(e.toString());
    }
  }
```

### Delete File

Deleting a file works in much the same way as adding a file, we use the item key to remove the file from the cloud. The calls to setState trigger a rebuild that populates the interface with an up-to-date file list.

```dart

  void deleteFile(StorageItem item) async {
    try {
      
      await Amplify.Storage.remove(
        key: item.key,
      );
      
    } catch (e) {
      Alert(
          context: context,
          type: AlertType.error,
          desc: "Error Deleting File: " + e.toString()).show();
    }
  }
```

### List Files

This is the moneymaker… 

Our storage bucket contains every single user’s files all stored in a bucket, meaning that they aren’t organized in any kind of database-like way. 

Since we can’t query a bucket, we have to filter our files based on user.username . With a single command (line 7), we can filter the complete list of files into a list that contains **only **files that belong to the current user.

Remember the FutureBuilder UI from earlier? This list is the future — when it is ready, the UI draws all of our files.

```dart
  Future<List<StorageItem>> listFiles() async {
    try {
      ListResult res = await Amplify.Storage.list();
      AuthUser user = await Amplify.Auth.getCurrentUser();

      List<StorageItem> items = res.items
          .where((e) => e.key.split('/').first.contains(user.username))
          .toList();

      return items;
    } catch (e) {
      Alert(
          context: context,
          type: AlertType.error,
          desc: "Error Listing Files: " + e.toString()).show();

      // return an empty list if something fails
      return List<StorageItem>(0);
    }
  }
```
## RESULTS

Uploading a file works as expected.

![](https://cdn-images-1.medium.com/max/2000/1*nkdmWcvJVX5RQM1T4UvOFg.gif)

Launch the analytics platform using amplify analytics console . For a more detailed look on using AWS analytics check out [this article](https://medium.com/@vennify.education/flutter-apps-with-aws-backend-part-3-analytics-bdb8b5bad5c0) that I wrote.

![The number of uploaded bytes are being tracked in Amazon Pinpoint](https://cdn-images-1.medium.com/max/3210/1*mcOksQn-lvX_gRHOkawmzQ.png)*The number of uploaded bytes are being tracked in Amazon Pinpoint*


## Future Work

Going from a bucket to a datastore is the next logical evolution of this app. Adding a feature to give another user (referenced by email) access to a particular file would also be a useful addition

If you want to take a stab at adding a new feature, feel free to open a pull request on the git [repo ](https://github.com/Vennify-Inc/DoogleGrive/)— If you have read this far I trust that you have the perseverance to make a worthwhile contribution. 

Amplify for Flutter is still a novel technology — and it shows in the lack of online content. In the coming months, I will produce more applications that use Amplify and demonstrate how to implement many of the core features modern apps require.

Until then, enjoy your open-source file storage app. 

### Acknowledgments and Sources

* GITHUB REPO: [https://github.com/Vennify-Inc/DoogleGrive](https://github.com/Vennify-Inc/DoogleGrive)

* [https://docs.amplify.aws/start/q/integration/flutter](https://docs.amplify.aws/start/q/integration/flutter)

* [https://github.com/aws-amplify/amplify-flutter](https://github.com/aws-amplify/amplify-flutter)

* [https://www.youtube.com/channel/UCpWACV_PAtwkTfzWKVM-dMA](https://www.youtube.com/channel/UCpWACV_PAtwkTfzWKVM-dMA)

* [https://dev.to/vennifymedia/flutter-apps-with-aws-amplify-backend-part-1-basic-setup-3g8d](https://dev.to/vennifymedia/flutter-apps-with-aws-amplify-backend-part-1-basic-setup-3g8d)

* [https://github.com/besharpsrl/flutter-amplify-demo](https://github.com/besharpsrl/flutter-amplify-demo)

* [https://pub.dev/packages/flutter_login](https://pub.dev/packages/flutter_login)

* [https://pub.dev/packages/flutter_downloader](https://pub.dev/packages/flutter_downloader)

* [https://pub.dev/packages/permission_handler](https://pub.dev/packages/permission_handler)