Starter Architecture for Flutter & Firebase Apps

Starter Architecture for Flutter & Firebase Apps

In this tutorial I give a detailed overview of a production-ready architecture. You can use the included starter project as the foundation for your Flutter & Firebase apps. What is architecture? Intro to Architecture. Good architecture for Flutter & Firebase apps. Important principles and desirable properties that we want in our code, with practical examples.

In this tutorial I give a detailed overview of a production-ready architecture that I've fine-tuned over the last two years. You can use the included starter project as the foundation for your Flutter & Firebase apps.

Motivation

Flutter & Firebase are a great combo for getting apps to market in record time.

Without a sound architecture, codebases can quickly become hard to test, maintain, and reason about. This severely impacts the development speed, and results in buggy products, sad developers and unhappy users.

I have already witnessed this first-hand with various client projects, where the lack of a formal architecture led to days, weeks - even months of extra work.

Is "architecture" hard? How can one find the "right" or "correct" architecture in the ever-changing landscape of front-end development?

Every app has different requirements, so does the "right" architecture even exist in the first place?

While I don't claim to have a silver bullet, I have refined and fine-tuned a production-ready architecture that I have already used in multiple Flutter & Firebase apps.

We will explore this and see how it's used in practice in the time tracker application included with the starter project:

So grab a drink & sit comfortably. And let's dive in!

Overview

We will start with an overview:

  • what is architecture and why we need it.
  • the importance of composition in good architecture.
  • good things that happen when you do have a good architecture.
  • bad things that happen when you don't have a good architecture.

Then we will focus on good architecture for Flutter & Firebase apps, and talk about:

  • application layers
  • unidirectional data flow
  • mutable and immutable state
  • stream-based architecture

I will explain some important principles, and desirable properties that we want in our code.

And we will see how everything fits together with some practical examples.

What you read here is the result of over two years of my own work, learning concepts, writing code, and refining it across multiple personal and client projects.

Ready? Let's go! 🚀

What is architecture?

I like to think of architecture as the foundation that holds everything together, and supports your codebase as it grows.

If you have a good foundation, it becomes easier to make changes and add new things.

Architecture uses design patterns to solve problems efficiently.

And you have to choose the design patterns that are most appropriate for the problem that you're trying to solve.

For example, an e-commerce application and a chat app will have very different requirements.

Composition

Regardless of what you're trying to build, it's likely that you'll have a set of problems, and you need to break them up into smaller, more manageable ones.

You can create basic building blocks for each problem, and you can build your app by composing blocks together. In fact:

Composition is a fundamental principle that is used extensively in Flutter, and more widely in software development.

Since we're here to build Flutter apps, what kind of building blocks do we need?

Example: Sign-in page

Let's say that you're building a page for email & password sign-in.

You will need some input fields and a button, and you need to compose these inputs together to make a form.

But the form by itself doesn't really do much.

You will also need to talk to an authentication service. The code for that is very different from your UI code.

To build this feature, you'll need code for UI, input validation and authentication:

Good architecture

The sign-in page above has a good architecture if it's made with well-defined building blocks (or components) that we can compose together.

We can take this same approach and scale it up to the entire application. This has some very clear benefits:

  • Adding new features becomes easier, because you can build upon the foundation that you already have.
  • The codebase becomes easier to understand, and you're likely to spot some recurring patterns and conventions as you read the code.
  • Components have clear responsibilities and don't do too many things. This happens by design if your architecture is highly composable.
  • Entire classes of problems go away (more on this later).
  • You can have different kinds of components, by defining separate application layers for different areas of concern (UI, logic, services).

Not-so-good architecture 😅

If we fail to define a good architecture, we don't have clear conventions for how to structure our app.

The lack of composable components leads to code that has a lot of dependencies.

This kind of code is hard to understand. Adding new features becomes problematic, and it's not even clear where new code should go.

Some other potential issues are also common:

  • the app has a lot of mutable state, making it hard to know which widgets rebuild and when.
  • it's not clear when certain variables can or cannot be null, as they are passed across multiple widgets.

All these issues can significantly slow down development, and negate the productivity advantages that are common in Flutter.

Bottom line: good architecture is important.

Application layers

Here's a diagram that shows my architecture for Flutter & Firebase apps:

The dotted horizontal lines define some clear application layers.

I think it's a good idea to always think about them. When you write new code, you should ask yourself: where does this belong?


Example: if you're writing some UI code for a new feature, you're likely to be inside a widget class. Maybe you need to call some external web-service API when a button is pressed. In this case, you need to stop and think: where does my API code go?

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('New Job'),
      actions: [
        FlatButton(
          child: Text('Save'),
          onPressed: () {
            // web API call here. Where should this code go?
          },
        ),
      ],
    ),
    body: _buildContents(),
  );
}

Thinking in terms of application layers is really helpful here.

This boils down to the single responsibility principle: each component in your app should do one thing only.

UI and networking code are two completely different things. They should not belong together, and they live in very different places.

Unidirectional data flow

In the diagram above, data flows from the outside world, into the services, view models, and all the way down to the widgets.

The call flow goes into the opposite direction. Widgets may call methods inside the view models and services. In turn, these may call APIs inside external Dart packages.

Very important: components that live on a certain application layer do not know about the existence of components in the layers below.

View models do not reference any widgets objects (or import any UI code for that matter). Instead:

Widgets subscribe themselves as listeners, while view models publish updates when something changes.

This is known as the publish/subscribe pattern, and it has various implementations in Flutter. You have already used this if you have ChangeNotifiers or BLoCs in your apps.


To connect everything together, we can use the Provider package. In fairness, it can take some time to get familiar with all the different kinds of providers. But we can use them to enforce an unidirectional data flow with immutable model classes. This has various benefits, which are discussed below.

For reference, here is a simplified diagram of the widget tree for the application included in the starter project:

This should give you a high-level understanding of how Provider is used in this project.

And it uses MultiProvider to group together multiple services and values. For more advanced use cases, you can use ProxyProvider.

Mutable and immutable state

One important aspect of this architecture lies in the differences between services and view models. In particular:

  • View models can hold and modify state.
  • Services can't.

In other words, we can think of services as pure, functional components.

Services can transform the data they receive from external Dart packages, and make it available to the rest of the app via domain-specific APIs.

For example, when working with Firestore we can use a wrapper service to do serialization:

  • Data in (read): This transforms streams of key-value pairs from Firestore documents into strongly-typed immutable data models.
  • Data out (write): This converts data models back to key-value pairs for writing to Firestore.

On the other hand, view models contain the business logic for your application, and are likely to hold mutable state.

This is ok, because widgets can be notified of state changes and rebuild themselves, according to the publish/subscribe pattern described above.

By combining the uni-directional data flow with the publish/subscribe pattern, we can minimise mutable application state, along with the problems that often come with it.

Stream-based architecture

Unlike traditional REST APIs, with Firebase we can build realtime apps.

That's because Firebase can push updates directly to subscribed clients when something changes.

For example, widgets can rebuild themselves when certain Firestore documents or collections are updated.

Many Firebase APIs are inherently stream-based. As a result, the simplest way of making our widgets reactive is to use StreamBuilder (or StreamProvider).

Yes, you could use ChangeNotifier or other state management techniques that implement observables/listeners.

But you would need additional "glue" code if you wanted to "convert" your input streams into reactive models based on ChangeNotifier.

Note: streams are the default way of pushing changes not only with Firebase, but with many other services as well. For example, you can get location updates with the onLocationChanged() stream of the location package. Whether you use Firestore, or want to get data from your device's input sensors, streams are the most convenient way of delivering asynchronous data over time.


In summary, this architecture defines separate application layers with an unidirectional data flow. Data is read from Firebase via streams, and widgets are rebuilt according to the publish/subscribe pattern.

Desirable code properties

When used correctly, this architecture leads to code that is:

  • clear
  • reusable
  • scalable
  • testable
  • performant
  • maintainable

Let's look at each point with some examples:

Clear

Suppose we want to build a page that shows a list of jobs.

Here is how I have implemented this in my project (explanation below):

class JobsPage extends StatelessWidget {
  Future<void> _delete(BuildContext context, Job job) async {
    try {
      final database = Provider.of<FirestoreDatabase>(context, listen: false);
      // database call
      await database.deleteJob(job);
    } on PlatformException catch (e) {
      PlatformExceptionAlertDialog(
        title: 'Operation failed',
        exception: e,
      ).show(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(Strings.jobs),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.add, color: Colors.white),
            // navigation call
            onPressed: () => EditJobPage.show(context),
          ),
        ],
      ),
      body: _buildContents(context),
    );
  }

  Widget _buildContents(BuildContext context) {
    final database = Provider.of<FirestoreDatabase>(context, listen: false);
    // Read jobsStream from Firestore & build UI when updated
    return StreamBuilder<List<Job>>(
      stream: database.jobsStream(),
      builder: (context, snapshot) {
        // Generic widget for showing a list of items
        return ListItemsBuilder<Job>(
          snapshot: snapshot,
          itemBuilder: (context, job) => Dismissible(
            key: Key('job-${job.id}'),
            background: Container(color: Colors.red),
            direction: DismissDirection.endToStart,
            // database call
            onDismissed: (direction) => _delete(context, job),
            child: JobListTile(
              job: job,
              // navigation call
              onTap: () => JobEntriesPage.show(context, job),
            ),
          ),
        );
      },
    );
  }
}

The build() method returns a scaffold with an AppBar.

The _buildContents() method returns a StreamBuilder, that is used to read some data as a stream from Firestore.

Inside it, we can pass the snapshot to a ListItemsBuilder, which is a generic widget (that I created) for showing a list of items.

In just 50 lines, this widget shows a list of items, and handles three different callbacks for:

  • creating a new job
  • deleting an existing job
  • routing to a job details page

Each of these operations only requires one line of code, because it delegates the actual work to external classes.

As a result this code is clear and readable.

It would be much harder to make sense of everything if we had database code, serialization, routing and UI all in one class. And our code would also be less reusable as a result.

Reusable

Here's the code for a different page, which shows a daily breakdown of all the jobs along with the pay:

class EntriesPage extends StatelessWidget {
  static Widget create(BuildContext context) {
    final database = Provider.of<FirestoreDatabase>(context, listen: false);
    return Provider<EntriesViewModel>(
      create: (_) => EntriesViewModel(database: database),
      child: EntriesPage(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(Strings.entries),
        elevation: 2.0,
      ),
      body: _buildContents(context),
    );
  }

  Widget _buildContents(BuildContext context) {
    final vm = Provider.of<EntriesViewModel>(context);
    return StreamBuilder<List<EntriesListTileModel>>(
      stream: vm.entriesTileModelStream,
      builder: (context, snapshot) {
        return ListItemsBuilder<EntriesListTileModel>(
          snapshot: snapshot,
          itemBuilder: (context, model) => EntriesListTile(model: model),
        );
      },
    );
  }
}

Note how I'm reusing some of the same components that I had in the previous page.

I use my ListItemsBuilder widget again, this time with a different model type (EntriesListTileModel).

And this time, the StreamBuilder's input stream comes from an EntriesViewModel.

But the data flows into the UI in the same way as before.

Bottom line: it pays off to build reusable components that can be used in multiple places.

Scalable

Let's talk about scalable code. If you have implemented Firestore CRUD operations before, you're probably familiar with this kind of syntax:

final ref = Firestore.instance.collection('users').document(uid).collection('jobs');
final snapshots = ref.snapshots();
// TODO: Manipulate snapshots stream and read documents' data

This can get quite unwieldly, especially if your documents have a lot of key-value pairs.

You don't want to have code like this inside your widgets, or even in your view models.

Rather, you can define a domain-specific Firestore API using some service classes, and keep things tidy.

Here is a FirestorePath class that I have created to list all possible read/write locations in my Firestore database:

class FirestorePath {
  static String job(String uid, String jobId) => 'users/$uid/jobs/$jobId';
  static String jobs(String uid) => 'users/$uid/jobs';
  static String entry(String uid, String entryId) =>
      'users/$uid/entries/$entryId';
  static String entries(String uid) => 'users/$uid/entries';
}

Alongside this, I have a FirestoreDatabase class, which I use to provide access to the various documents and collections.

class FirestoreDatabase {
  FirestoreDatabase({@required this.uid}) : assert(uid != null);
  final String uid;

  // CRUD operations - implementations omitted for simplicity
  Future<void> setJob(Job job) { ... }
  Future<void> deleteJob(Job job) { ... }
  Stream<Job> jobStream({@required String jobId}) { ... }
  Stream<List<Job>> jobsStream() { ... }
  Future<void> setEntry(Entry entry) { ... }
  Future<void> deleteEntry(Entry entry) { ... }
  Stream<List<Entry>> entriesStream({Job job}) { ... }
}

This class exposes all the various CRUD operations to the rest of the app, behind a nice API that uses strongly-typed model classes.

With this setup, adding a new type of document or collection in Firestore becomes a repeatable process:

  • add some additional paths to FirestorePath
  • add the corresponding Future and Stream based APIs to FirestoreDatabase to support the various operations
  • create strongly-typed model classes as needed. These include the serialization code for the new kind of documents that I need to use.

All of this code remains confined inside a services folder in my project.

Widgets can use the new database APIs with Provider:

final database = Provider.of<FirestoreDatabase>(context, listen: false);
// TODO: call database APIs as needed

The code above is easily scalable. I can add new functionality by following repeatable steps, and ensure that the code is consistent. This is very valuable if you work in a team.

Testable

This architecture leads to testable code.

This is true for my unit tests, because my classes are small and have few dependencies.

But it also applies to widget tests as well, because all the widgets have scoped access to the dependencies that they need.

And thanks to the Provider package, it is easy to swap out services classes with mock objects and run tests against them:

void main() {
  MockAuthService mockAuthService;
  MockDatabase mockDatabase;

  setUp(() {
    mockAuthService = MockAuthService();
    mockDatabase = MockDatabase();
  });

  Future<void> pumpAuthWidget(
      WidgetTester tester,
      {@required
          Widget Function(BuildContext, AsyncSnapshot<User>) builder}) async {
    await tester.pumpWidget(
      Provider<FirebaseAuthService>(
        create: (_) => mockAuthService,
        child: AuthWidgetBuilder(
          databaseBuilder: (_, uid) => mockDatabase,
          builder: builder,
        ),
      ),
    );
    await tester.pump(Duration.zero);
  }

  // TODO: Widget tests here
}

This leads to widget tests are fast and predictable, because they don't call any networking code.

With the right setup it's even possible to make the entire app testable, as long as we can swap out any services with mock objects at the root of the widget tree.

This is particularly useful when running integration tests, that can be used to test entire user flows in the app.

Performant

One great thing about this architecture is that it minimises widget rebuids.

This is accomplished using Provider and StreamBuilder/FutureBuilder as needed.

How?

Well, once we read some state or data asynchronously from an external service, we can make that available synchronously to all descendant widgets.

For example, this app requires the user to sign-in with Firebase. This code returns either the HomePage or the SignInPage depending on the authentication status of the user:

@override
Widget build(BuildContext context) {
  final authService =
      Provider.of<FirebaseAuthService>(context, listen: false);
  return StreamBuilder<User>(
    // asynchronous data in
    stream: authService.onAuthStateChanged,
    builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
      final User user = snapshot.data;
      if (user != null) {
        // make data available synchronously to all descendants
        return MultiProvider(
          providers: [
            Provider<User>.value(value: user),
            Provider<FirestoreDatabase>(
              create: (_) => FirestoreDatabase(user.uid),
            ),
          ],
          // HomePage and all descendant widgets can get the current user with `Provider.of<User>(context)`,
          // rather than `await FirebaseAuth.instance.currentUser()`
          child: HomePage(),
        );
      }
      return SignInPage();
    },
  );
}

Once a non-null User object is extracted from the snapshot, it can be made available synchronously to all descendants:

Widget build(BuildContext context) {
  final user = Provider.of<User>(context);
  // show UI
}

The code above is much better than this:

Widget build(BuildContext context) {
  // so much boilerplate code 😅
  return FutureBuilder<FirebaseUser>(
    future: FirebaseAuth.instance.currentUser(),
    builder: (context, snapshot) {
      if (snapshot.data != null) {
        // show UI
      }
      return CircularProgressIndicator();
    },
  );
}

Here we create a FutureBuilder, just so that we can get the current user asynchronously. This is unnecessary, because we have already obtained it in one of the ancestor widgets. And we have to write more boilerplate code in the builder, and show some loading indicator until this Future returns.

Instead, Provider can solve this problem for us, and we should use it to our advantage. > Bottom line: we can use Provider to minimise widget rebuilds, avoid any unnecessary API calls to Firebase, and reduce boilerplate code.

Maintainable

This architecture leads to maintainable code, and the examples above should serve as evidence.

Maintainable code will save you (and your team) days, weeks and months of extra effort.

Beyond that, your code will be much nicer to work with, and you'll sleep better at night. 😴

Conclusion

I hope that this overview has inspired you to invest in good architecture.

If you're starting a new project, consider planning out your architecture upfront, based on your requirements.

If you're struggling with a codebase that doesn't follow good software design principles, start refactoring in small iterations. You don't have to fix everything at once, but it helps to move slowly towards your desired architecture.

And if you are building a project with Flutter and Firebase or any other kind of streaming APIs, do check out my starter project on GitHub. This is a complete time tracking application:

The README is a good place to get more familiar with all the concepts that we covered.

After that, you can take a look at the source code, run the project (note: Firebase configuration needed), and get a good understanding of how everything fits together.

And if you want to learn all these principles more in depth, and build the time-tracking application from scratch, then there's no better place than my Flutter & Firebase course.

Happy coding!

Firebase Flutter Tutorial - Firebase CloudStorage in Flutter

Firebase Flutter Tutorial - Firebase CloudStorage in Flutter

In this Firebase Flutter Tutorial, we wrap the Firebase Storage package into a service to easily manage and upload the firebase storage.

Hello there Flutter Dev 🙋‍♂️ In this tutorial we will be going over Cloudstorge on Firebase and how to integrate that with your mobile application.

Today we'll do a simple task that is probably a very common task in mobile app development. We will provide the user with a UI to select and upload a photo, save that photo in a post and display that to them in a collection. The photo will be stored in Firebase CloudStorage and we'll use a URL to the photo to display to users in the app. We'll start off by updating the UI to allow us to upload a photo.

Cloud Storage Setup

Before we start with the code lets setup our cloud storage. Open up the firebase console and click on the storage icon in the left toolbar. Click on create bucket, choose the location and continue. You will now have what is called a "bucket" where you can store files. You can think of this as a hard drive that you access through a url web request. Each of the files here will have an access token / url that you can access only through that url with the attached token. You can set visibility by controling the access level for that download url token. This is the url we'll use to display the image in the app.

Implementation

Let go over a quick implementation overview. We'll create a service that wraps the provided firebase storage package. This service will take in a File object and a title and upload that to the storage. When the operation is complete we will return the url which is what we'll put inside of our post as the imageUrl. The file that we're passing in will be selected using the UI presented by the image picker library. Let's get to it.

Code setup

We start off by adding the firebase_storage and the image_picker package to the pubspec.

firebase_storage: ^3.1.1
image_picker: ^0.6.3+1

Firebase storage is to interact with the Firebase Cloud Storage, the image picker is to show the user a UI that will allow them to select an image from their device.

Cloud Storage Implementation

Under the services folder create a new file called cloud_storage_service.dart. We'll give it a function called uploadImage that Takes in a required file as well as a title. You can pass in the UID, or anything you'd like to identify your images by.

import 'package:firebase_storage/firebase_storage.dart';

class CloudStorageService {
  Future<CloudStorageResult> uploadImage({
    @required File imageToUpload,
    @required String title,
  }) async {

  }
}

class CloudStorageResult {
  final String imageUrl;
  final String imageFileName;

  CloudStorageResult({this.imageUrl, this.imageFileName});
}

To access the Firestore Storage instance we use the FirebaseStorage.instance static property. The storage library works similar to the firebase documents. You can get a reference to a file that doesn't exist yet and then add the data in there that you want. We'll get a reference to our future file using the title and the date epoch to keep it unique. Once we have our reference we will call putFile and pass it in the selected File. This will give us a StorageUploadTask. This object has an onComplete Future that returns a StorageTaskSnapshot (similar to firebase snapshot). We can await that future and once we have the snapshot we can use the StorageReference returned and get the downloadUrl. We'll return the url when the task is complete or null.

  Future<CloudStorageResult> uploadImage({
    @required File imageToUpload,
    @required String title,
  }) async {

    var imageFileName = title + DateTime.now().millisecondsSinceEpoch.toString();

    final StorageReference firebaseStorageRef = FirebaseStorage.instance
    .ref()
    .child(imageFileName);

    StorageUploadTask uploadTask = firebaseStorageRef.putFile(imageToUpload);

    StorageTaskSnapshot storageSnapshot = await uploadTask.onComplete;

    var downloadUrl = await storageSnapshot.ref.getDownloadURL();

    if (uploadTask.isComplete) {
      var url = downloadUrl.toString();
      return CloudStorageResult(
        imageUrl: url,
        imageFileName: imageFileName,
        );
    }

    return null;
  }

Open up the locator.dart file and register the CloudStorageService with the get_it instance.

locator.registerLazySingleton(() => CloudStorageService());

Image Selection Implementation

We'll start off by wrapping the ImagePicker library into our own class. This way our business logic is not dependent on any third party packages. It's something I like to do, if you go the additional step and add it behind an interface then you can mock it out during testing as well.

Create a new folder called utils. Inside create a new file called image_selector.dart

import 'package:image_picker/image_picker.dart';

class ImageSelector {
  Future<File> selectImage() async {
    return await ImagePicker.pickImage(source: ImageSource.gallery);
  }

}

I know it seems silly to have a class that wraps one line, but you can do much more with it than this. You can keep the file in memory until you're certain it's uploaded, you can have different sources passed in from different functions, etc. The main reason for this is to remove the dependency of ImagePicker from any of the code in the app that has to make use of the functionality.

Open up the locator.dart file and register the ImageSelector with the get_it instance.

locator.registerLazySingleton(() => ImageSelector());

Finally open up the CreatePostViewModel where we'll locate the selector and then make use of it in a function called selectAndUploadImage. We'll also import the CloudStorageService for later use. We'll use the selectImage function to set the image to upload and display that to the user in the UI.

class CreatePostViewModel extends BaseModel {
  final ImageSelector _imageSelector = locator<ImageSelector>();
  final CloudStorageService _cloudStorageService = locator<CloudStorageService>();

  File _selectedImage;
  File get selectedImage => _selectedImage;

  Future selectImage() async {
    var tempImage = await _imageSelector.selectImage();
    if(tempImage != null) {
      _selectedImage = tempImage;
      notifyListeners();
    }
  }
}

In the same viewmodel update the addPost function to upload the image if we're not editting the post. We'll then use that url as the imageUrl in the post. For error handling I would show a snack bar if the imageUrl comes back null that indicates to the user that the image upload has failed.

 Future addPost({@required String title}) async {
    setBusy(true);

    CloudStorageResult storageResult;

    if (!_editting) {
      storageResult = await _cloudStorageService.uploadImage(
          imageToUpload: _selectedImage, title: title);
    }

    var result;

     if (!_editting) {
      result = await _firestoreService.addPost(Post(
        title: title,
        userId: currentUser.id,
        imageUrl:  storageResult.imageUrl,
        imageFileName: storageResult.imageFileName
      ));
    } else {
      result = await _firestoreService.updatePost(Post(
        title: title,
        userId: _edittingPost.userId,
        documentId: _edittingPost.documentId,
        imageUrl: _edittingPost.imageUrl,
        imageFileName: _edittingPost.imageFileName,
      ));
    }

    ...
  }

Next, open the Post model and add the new imageFileName String that we'll use to later delete the post.

class Post {
  ...
  final String imageFileName;

  Post({
    ...
    this.imageFileName,
  });

  Map<String, dynamic> toMap() {
    return {
      ...
      'imageFileName': imageFileName,
    };
  }

  static Post fromMap(Map<String, dynamic> map, String documentId) {
    if (map == null) return null;

    return Post(
      ...
      imageFileName: map['imageFileName'],
      documentId: documentId,
    );
  }
}

Now we can go onto the UI for the functionality. First thing to do is update the CreatePostView and add a gesture detector onto the grey rectangle we're displaying. When tapped we'll call the selectImage function. We'll also add a conditional to make sure when an image is selected we show it in that grey block. Update the container in the create_post_view that has the text in it to the following.

GestureDetector(
  // When we tap we call selectImage
  onTap: () => model.selectImage(),
  child: Container(
    height: 250,
    decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(10)),
    alignment: Alignment.center,
    // If the selected image is null we show "Tap to add post image"
    child: model.selectedImage == null
        ? Text(
            'Tap to add post image',
            style: TextStyle(color: Colors.grey[400]),
          )
          // If we have a selected image we want to show it
        : Image.file(model.selectedImage),
  ),
)

If you run the app now, tap on the FAB, enter a title and tap on the image block you'll see the image picker pop up. Select an image and it should be showing in the grey block in place of the text :) Add the post by pressing the FAB and it'll send it up to the cloud and return you a url.

If you open up the cloud storage now you'll see a file with the title you enetered and a number after it. That's the image you uploaded. Next up is displaying the image.

Image display implementation

To display the images from the cloud storage we will use the cached_network_image package. Add it to your pubspec.

cached_network_image: ^2.0.0

Open up the post_item and we'll update the UI. First thing is to make sure when we have an image we don't want to give the list a fixed size. We'll check if there's an image. If there's an image we set the heigh to null (meaning wrap content), otherwise we set it to 60.

class PostItem extends StatelessWidget {
  final Post post;
  ...
  @override
  Widget build(BuildContext context) {
    return Container(
      // Check if we have an image and set it to null or 60
      height: post.imageUrl != null ? null : 60,
      margin: const EdgeInsets.only(top: 20),
      alignment: Alignment.center,
      child: Row(
        children: <Widget>[
          Expanded(
              child: Padding(
            padding: const EdgeInsets.only(left: 15.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                // If the image is not null load the imageURL
                post.imageUrl != null
                    ? SizedBox(
                        height: 250,
                        child: CachedNetworkImage(
                          imageUrl: post.imageUrl,
                          placeholder: (context, url) =>
                              CircularProgressIndicator(),
                          errorWidget: (context, url, error) =>
                              Icon(Icons.error),
                        ),
                      )
                // If the image is null show nothing
                    : Container(),
                Text(post.title),
              ],
            ),
          )),
         ...
        ],
      ),
      ...
    );
  }
}

That's it. You can now, post an image to the cloud storage. Then see it load as an item in the list of posts :)

Delete on Cloud Storage

Last thing to do is to delete the image again when a post is removed. This can be done by simple getting the ref and calling delete on it. Open up the CloudStorageService and we'll add a delete function.

class CloudStorageService {
   Future deleteImage(String imageFileName) async {
    final StorageReference firebaseStorageRef =
        FirebaseStorage.instance.ref().child(imageFileName);

    try {
      await firebaseStorageRef.delete();
      return true;
    } catch (e) {
      return e.toString();
    }
  }
}

Open up the HomeViewModel, locate the CloudStorageService and then after deleting the post from the firestore db call the delete function on the CloudStorageService as well.

class HomeViewModel extends BaseModel {
  final CloudStorageService _cloudStorageService = locator<CloudStorageService>();

  Future deletePost(int index) async {
    var dialogResponse = await _dialogService.showConfirmationDialog(
      title: 'Are you sure?',
      description: 'Do you really want to delete the post?',
      confirmationTitle: 'Yes',
      cancelTitle: 'No',
    );

    if (dialogResponse.confirmed) {
      var postToDelete = _posts[index];
      setBusy(true);
      await _firestoreService.deletePost(postToDelete.documentId);
      // Delete the image after the post is deleted
      await _cloudStorageService.deleteImage(postToDelete.imageFileName);
      setBusy(false);
    }
  }

}

And That's it. Basic Cloud storage functionality wrapped into a service for easy use. Make sure to follow me on Youtube for the rest of the series. Until next week :)

Flutter for Beginners 2020 - Build a Flutter App with Google's Flutter & Dart

Flutter for Beginners 2020 - Build a Flutter App with Google's Flutter & Dart

Flutter tutorial for Beginners 2020 - Build a Flutter App with Google's Flutter & Dart. Flutter Introduction for Beginners: Get Started with Flutter and learn how to build an iOS and Android app with Flutter! What is Flutter? How Flutter & Dart Code Gets Compiled? Why Choose Flutter? Flutter is Google's UI toolkit for crafting beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Free Flutter Introduction for Beginners: Get Started with Flutter and learn how to build an iOS and Android app with Flutter!


Content:

  • Introduction 00:03
  • What is Flutter 01:50
  • Flutter's Architecture 07:44
  • How Flutter & Dart Code Gets Compiled
  • To Native Apps 12:31
  • Understanding Flutter Versions 15:44
  • Flutter macOS Setup 18:34
  • macOS Development Environment 37:24
  • Flutter Windows Setup 41:32
  • Windows Development Environment 01:00:48
  • Flutter & Material Design 01:04:34
  • Flutter Alternatives 01:05:49
  • Course Outline 01:11:56
  • How To Get The Most Out Of The
  • Course 01:18:36
  • Module Introduction 01:21:17
  • Creating a New Project 01:23:42
  • An Overview of the Generated Files &
  • Folders 01:35:57
  • Analyzing the Default App 01:44:18
  • Dart Basics 01:49:50
  • More Dart Basics 02:07:08
  • Building an App From Scratch 02:20:15
  • Running the App on an Emulator 02:31:18
  • Class Constructors & Named
  • Arguments 02:34:43
  • First Summary & Additional Syntax 02:43:13
  • Building a Widget Tree 02:51:24
  • Visible (Input / Output) & Invisible
  • (Layout / Control) Widgets 02:59:04
  • Adding Layout Widgets 03:02:17
  • Connecting Functions & Buttons 03:09:09
  • Anonymous Functions 03:16:40
  • Updating Widget Data (Or: Using
  • StatelessWidget Incorrectly) 03:20:01
  • Updating Correctly with Stateful
  • Widgets 03:26:38
  • A Brief Look Under The Hood 03:38:36
  • Using Private Properties 03:41:04
  • Creating a New, Custom Widget 03:46:21
  • First Styling & Layouting Steps 03:58:11
  • Enums & Multiple Constructors 04:06:12
  • Official Docs & The Widget Catalog 04:10:19
  • Passing Callback Functions Around 04:12:59
  • Introducing Maps 04:23:37
  • Mapping Lists to Widgets 04:29:30
  • final vs const 04:39:54
  • Introducing "if" Statements 04:50:45
  • [DART DEEP DIVE] More on "if"
  • Statements 04:57:36
  • [DART DEEP DIVE] The "null" Value 05:08:52
  • Outputting Widgets Conditionally 05:10:55
  • Splitting the App Into Widgets 05:13:16
  • Calculating a Total Score 05:23:13
  • Getters & "else-if" 05:30:59
  • Resetting the Quiz 05:37:34
  • Wrap Up 05:42:28

Apple Sign In with Flutter & Firebase Authentication

Apple Sign In with Flutter & Firebase Authentication

In this tutorial we'll see how to add Apple Sign In to our Flutter apps from scratch. Learn how to implement Apple Sign In with Flutter & Firebase Authentication (from scratch), and give your iOS users a convenient way of signing into your app. Apple Sign In with Flutter & Firebase Authentication.

In this tutorial we'll see how to add Apple Sign In to our Flutter apps from scratch.

Apple Sign In is a new authentication method that is available on iOS 13 and above.

It is very convenient, as your iOS users already have an Apple ID, and can use it to sign in with your app.

So just as you would offer Google Sign In on Android, it makes sense to offer Apple Sign In on iOS.

We will use the Apple Sign In Flutter plugin available on pub.dev.

Note: this plugin supports iOS only, and you can only use this on devices running iOS 13 and above.

Prerequisites

  • Xcode 11 installed
  • An Apple Developer Account
  • An iOS 13.x device or simulator, signed in with an Apple ID
Project Setup

After creating a new Flutter project, add the following dependencies to your pubspec.yaml file:

dependencies:
  firebase_auth: ^0.15.3
  apple_sign_in: ^0.1.0
  provider: ^4.0.1

Note: we will use Provider for dependency injection, but you can use something else if you prefer.

Next, we need add Firebase to our Flutter app. Follow this guide for how to do this:

After we have followed the required steps, the GoogleService-Info.plist file should be added to our Xcode project.

And while in Xcode 11, select the Signing & Capabilities tab, and add "Sign In With Apple" as a new Capability:

Once this is done, ensure to select a team on the Code Signing section:

This will generate and configure an App ID in the "Certificates, Identifiers & Profiles" section of the Apple Developer portal. If you don't do this, sign-in won't work.

As a last step, we need to enable Apple Sign In in Firebase. This can be done under Authentication -> Sign-in method:

This completes the setup for Apple Sign In, and we can dive into the code.

Checking if Apple Sign In is available

Before we add the UI code, let's write a simple class to check if Apple Sign In is available:

import 'package:apple_sign_in/apple_sign_in.dart';

class AppleSignInAvailable {
  AppleSignInAvailable(this.isAvailable);
  final bool isAvailable;

  static Future<AppleSignInAvailable> check() async {
    return AppleSignInAvailable(await AppleSignIn.isAvailable());
  }
}

Then, in our main.dart file, let's modify the entry point:

void main() async {
  // Fix for: Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
  WidgetsFlutterBinding.ensureInitialized();
  final appleSignInAvailable = await AppleSignInAvailable.check();
  runApp(Provider<AppleSignInAvailable>.value(
    value: appleSignInAvailable,
    child: MyApp(),
  ));
}

The first line prevents an exception that occurs if we attempt to send messages across the platform channels before the binding is initialized.

Then, we check if Apple Sign In is available by using the class we just created.

And we use Provider to make this available as a value to all widgets in our app.

Note: this check is done upfront so that appleSignInAvailable is available synchronously to the entire widget tree. This avoids using a FutureBuilder in widgets that need to perform this check.

Adding the UI code

Instead of the default counter app, we want to show a Sign In Page with a button:

import 'package:apple_sign_in/apple_sign_in.dart';
import 'package:apple_sign_in_firebase_flutter/apple_sign_in_available.dart';
import 'package:apple_sign_in_firebase_flutter/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class SignInPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final appleSignInAvailable =
        Provider.of<AppleSignInAvailable>(context, listen: false);
    return Scaffold(
      appBar: AppBar(
        title: Text('Sign In'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(6.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [            
            if (appleSignInAvailable.isAvailable)
              AppleSignInButton(
                style: ButtonStyle.black, // style as needed
                type: ButtonType.signIn, // style as needed
                onPressed: () {},
              ),
          ],
        ),
      ),
    );
  }
}

Note: we use a collection-if to only show the AppleSignInButton if Apple Sign In is available. See this video for UI-as-code operators in Dart.

Back to our main.dart file, we can update our root widget to use the SignInPage:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Apple Sign In with Firebase',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: SignInPage(),
    );
  }
}

At this stage, we can run the app on an iOS 13 simulator and get the following:

Adding the authentication code

Here is the full authentication service that we will use to sign in with Apple (explained below):

import 'package:apple_sign_in/apple_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/services.dart';

class AuthService {
  final _firebaseAuth = FirebaseAuth.instance;

  Future<FirebaseUser> signInWithApple({List<Scope> scopes = const []}) async {
    // 1\. perform the sign-in request
    final result = await AppleSignIn.performRequests(
        [AppleIdRequest(requestedScopes: scopes)]);
    // 2\. check the result
    switch (result.status) {
      case AuthorizationStatus.authorized:
        final appleIdCredential = result.credential;
        final oAuthProvider = OAuthProvider(providerId: 'apple.com');
        final credential = oAuthProvider.getCredential(
          idToken: String.fromCharCodes(appleIdCredential.identityToken),
          accessToken:
              String.fromCharCodes(appleIdCredential.authorizationCode),
        );
        final authResult = await _firebaseAuth.signInWithCredential(credential);
        final firebaseUser = authResult.user;
        if (scopes.contains(Scope.fullName)) {
          final updateUser = UserUpdateInfo();
          updateUser.displayName =
              '${appleIdCredential.fullName.givenName} ${appleIdCredential.fullName.familyName}';
          await firebaseUser.updateProfile(updateUser);
        }
        return firebaseUser;
      case AuthorizationStatus.error:
        print(result.error.toString());
        throw PlatformException(
          code: 'ERROR_AUTHORIZATION_DENIED',
          message: result.error.toString(),
        );

      case AuthorizationStatus.cancelled:
        throw PlatformException(
          code: 'ERROR_ABORTED_BY_USER',
          message: 'Sign in aborted by user',
        );
    }
    return null;
  }
}

First, we pass a List<Scope> argument to our method. Scopes are the kinds of contact information that can be requested from the user (email and fullName).

Then, we make a call to AppleSignIn.performRequests and await for the result.

Finally, we parse the result with a switch statement. The three possible cases are authorized, error and cancelled.

Authorized

If the request was authorized, we create an OAuthProvider credential with the identityToken and authorizationCode we received.

We then pass this to _firebaseAuth.signInWithCredential(), and get an AuthResult that we can use to extract the FirebaseUser.

And if we requested the full name, we can update the profile information of the FirebaseUser object with the fullName from the Apple ID credential.

Error or Cancelled

If authentication failed or was cancelled by the user, we throw a PlatformException that can be handled by at the calling site.

Using the authentication code

Now that our auth service is ready, we can add it to our app via Provider like so:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<AuthService>(
      create: (_) => AuthService(),
      child: MaterialApp(
        title: 'Apple Sign In with Firebase',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.indigo,
        ),
        home: SignInPage(),
      ),
    );
  }
}

Then, in our SignInPage, we can add a method to sign-in and handle any errors:

Future<void> _signInWithApple(BuildContext context) async {
  try {
    final authService = Provider.of<AuthService>(context, listen: false);
    final user = await authService.signInWithApple(
        requestEmail: true, requestFullName: true);
    print('uid: ${user.uid}');
  } catch (e) {
    // TODO: Show alert here
    print(e);
  }
}

Finally, we remember to call this on the callback of the AppleSignInButton:

AppleSignInButton(
  style: ButtonStyle.black,
  type: ButtonType.signIn,
  onPressed: () => _signInWithApple(context),
)
Testing things

Our implementation is complete, and we can run the app.

If we press the sign-in button and an Apple ID is not configured on our simulator or device, we will get the following:

After signing in with our Apple ID, we can try again, and we will get this:

After continuing, we are prompted to enter the password for our Apple ID (or use touch ID/face ID if enabled on the device). If we have requested full name and email access, the user will have a chance edit the name, and choose to share or hide the email:

After confirming this, the sign-in is complete and the app is authenticated with Firebase.

Note: if the sign-in screen is not dismissed after authenticating, it's likely because you forgot to set the team in the code signing options in Xcode.

The next logical step is to move away from the SignInPage and show the home page instead. This can be done by adding a widget above the SignInPage, to decide which page to show depending on the onAuthStateChaged stream of FirebaseAuth.

Congratulations, you have now enabled Apple Sign In in your Flutter app! Your iOS users are grateful. 🙏

Full Source Code is here on GitHub.

Happy coding!