Speedrun your MVP with Flutter and Firebase UI

christiannitas
Firebase Developers
7 min readAug 1, 2023
Dash and Firebase

If you are like me, you are always on the lookout for great ideas for a cool side project. Whenever I cook up a new idea that may have potential, I always go for Flutter + Firebase for the implementation because they work amazingly together.

However, whenever I start these projects, I get hit with a wall of tasks that I have to implement before actually implementing the business logic I think about. Most apps use some form of authentication, file uploads, pagination, or register screens, and without implementing them, you cannot see your MVP (Minimum Viable Product) realized.

That’s where Firebase UI for Flutter comes in, offering all developers seamless integration with the most popular services offered by Firebase and handling all the boilerplate for you so that you can get back to the code that really matters.

We are going to look at a number of widgets from the Firebase UI set of packages with which you can speedrun your MVP. In this article, we will implement a chat app on MacOS using these tools. You can find the full code on my GitHub:

Firebase Auth UI

Firebase already provides quick and easy-to-use solutions for authentication, but when it comes to authentication UI, it is up to you to implement all the necessary logic a user expects from any modern apps, like an auth screen, integration with 3rd party auth providers, register capabilities and even a profile screen for your user to see its details.

Firebase UI offers all of these functionalities as a pre-packaged set of widgets that can handle authentication with most providers, a register screen, and a profile screen. To use the sign-in screen, you can simply return the SignInScreen widget in your build method:

class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final providers = [EmailAuthProvider()];

return SignInScreen(
providers: providers,
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.pushReplacementNamed(context, '/chat');
}),
],
);
}
}

The widget can be configured with multiple authentication providers, like email, Google, Apple, Facebook, and many others. When the user enters its credentials and logs in, a new auth state is generated and the widget will look in the actions field to see if any objects match the new state generated. If so, then the provided callback is run. This screen also doubles as a register screen so your users can sign up on your app. In this example, we redirect the user to the chat route on successful login.

You can listen to multiple events including:

  • AuthFailed — when authentication fails, you can execute custom behaviors.
  • UserCreated — called when a user registers and is created in FirebaseAuth.
  • SignedIn — called on successful login.
Sign-In Screen
Register Screen

However, you may soon find that these screens do not fit your overall app style. Even though these widgets do not expose parameters for theming, we can wrap them with a Theme widget where we can override the default themes to match our overall style language:

class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final providers = [EmailAuthProvider()];

return Theme(
data: Theme.of(context).copyWith(
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
)
),
child: SignInScreen(
providers: providers,
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.pushReplacementNamed(context, '/chat');
}),
],
)
);
}
}
Customized Sign-In Screen

Firebase Firestore UI

Good. The user is now logged in to our application. Now what? Well, in comes the most versatile widget out of all of the ones presented here.

When you think of it, most data on mobile is displayed as a list of items the user can interact with. If you use Firestore, this list comes from a query performed on a collection. When developing your app, you must write the logic of getting your documents from Firebase and display them on the screen all by yourself, but the FirestoreListView widget will make this process nonexistent.

It works by taking a query on your collections and then mapping each document to a widget using a builder function provided by us:


class ChatPage extends StatefulWidget {
const ChatPage({super.key});

@override
State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
@override
Widget build(BuildContext context) {
final query = FirebaseFirestore.instance
.collection('chat')
.orderBy('createdAt', descending: true);

return Scaffold(
body: Column(
children: [
Expanded(
child: FirestoreListView(
reverse: true,
query: query,
itemBuilder: (context, item) => MessageWidget(item.data()),
),
),
SendMessageFooter(),
],
),
);
}
}

Here we wrote a page that will display chat messages from a collection. You can see how we simply create our query and then pass it to our list view, as well as a function that will return a message widget.

If we want however to write this properly, we would like to use a converter in order to transform our Firebase documents to our local model of a Message. To do this, we write two functions that convert our Message model to and from snapshots.

class Message {
String? id;
String name;
String text;
DateTime? createdAt;
String? senderId;

Message({
required this.text,
this.id,
name,
this.senderId,
this.createdAt,
}) : name = name ?? FirebaseAuth.instance.currentUser!.displayName;

factory Message.fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) =>
Message(
id: snap.id,
name: snap["name"],
text: snap["text"],
createdAt: (snap['createdAt'] as Timestamp?)?.toDate(),
senderId: snap['senderId'],
);

Map<String, dynamic> toJson() => {
if (id != null) 'id': id,
'name': name,
'text': text,
'createdAt': createdAt ?? FieldValue.serverTimestamp(),
'senderId': senderId ?? FirebaseAuth.instance.currentUser!.uid,
};
}

We will call these functions from our query and then we will pass our model to the ListView as a type argument for the widget to infer the actual type of objects in the item builder.

class _ChatPageState extends State<ChatPage> {
@override
Widget build(BuildContext context) {
final query = FirebaseFirestore.instance
.collection('chat')
.orderBy('createdAt', descending: true)
.withConverter<Message>(
fromFirestore: (json, _) => Message.fromSnapshot(json),
toFirestore: (obj, _) => obj.toJson(),
);
return Scaffold(
body: Column(
children: [
Expanded(
child: FirestoreListView<Message>(
reverse: true,
query: query,
itemBuilder: (context, item) => MessageWidget(item.data()),
),
),
SendMessageFooter(),
],
),
);
}
}

What is excellent about this widget is that it comes with reactivity built in. Whenever a user will send a message to the collection, all other users will receive it, thus keeping the widget up to date with the server.

The list also comes with a default page size and a default loading indicator, which can both be modified to fit the theme of the application, so that’s two problems to not worry about anymore when using this widget!

Firebase Storage UI

At the time of writing this, the UI library for Firebase storage is still in development, but even now in this early stage, it offers a few functionalities that are great for building apps. One of these functionalities is the upload button which is a widget that will handle the selection and uploading of files from your local device to your Firebase storage buckets.

UploadButton(
onError: (_, __) {},
onUploadComplete: (ref) async {
final download = await ref.getDownloadURL();
final msg = Message(text: download);
FirebaseFirestore.instance.collection('chat').add(
msg.toJson(),
);
}),

You can use it as any other button in your Flutter app, however, when pressed it will open a file picker and will allow you to upload that file to Firebase storage. If any error happens, the onError callback is called, and once it successfully uploaded the file, it will call the onUploadComplete call back which will provide us with the reference to that file. In the above example, we get the download URL for the file and then create a new message entity in our collection.

If you want to upload to a specific folder in your storage bucket, you can wrap the button in a FirebaseUIStorageConfigOverride widget which takes a config object of type FirebaseUIStorageConfiguration. Here you can change the uploadRoot property to suit your requirements.

FirebaseUIStorageConfigOverride(
config: FirebaseUIStorageConfiguration(
uploadRoot: FirebaseStorage.instance.ref('photos/'),
),
child: UploadButton(
onError: (_, __) {},
onUploadComplete: (ref) async {
final down = await ref.getDownloadURL();
final msg = Message(text: down);
FirebaseFirestore.instance.collection('chat').add(
msg.toJson(),
);
}),
),

And many more…

These few widgets offer you incredible speed and convenience when developing your next million-dollar startup, but the Firebase UI toolkit will only continue to grow. Right now you have many more widgets like the Firestore collection builder or storage list view so that you may use them in building your apps and getting them to your users as fast as possible.

This article was written as part of my presentation at the Bucharest Flutter Meetup on the same topic. I want to thank the community organizers and sponsors for making it possible for me to talk about what I am passionate about in real person, not only here on Medium.

--

--

christiannitas
Firebase Developers

Software Developer, passionate and writing about mobile, web, backend and anything I find interesting.