Trigger default email app to send email in Flutter

To use the old fashion mail-to functionality in Flutter that make the user’s phone open up Gmail or whatever is the default email application to start composing an email is actually pretty simple. Just like in ordinary JavaScript where you do something like this:

window.location.href = 'mailto:address@fileidea.com&subject=Hello there&body=This is my message';

You can do it in a similar way in Dart. In your Flutter project add the dependency in your pubspec.yaml file under dependencies:

url_launcher: ^6.0.9

Now if you have let’s say a ListView like this that shows items:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FileIdea Example',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: MyHomePage(title: 'FileIdea example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class EmailThing {
  late String title;
  late String email;

  EmailThing(title, email) {
    this.title = title;
    this.email = email;
  }
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final stuff = List<EmailThing>.generate(10,
        (i) => new EmailThing("thing ${i + 1}", "somerandomdomain$i@info.com"));

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ListView.builder(
          itemCount: stuff.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              onTap: () => {
                print(
                  "hello from fileidea: " + stuff[index].email,
                )
              },
              tileColor: Colors.green[200],
              title: Text('${stuff[index].title}'),
            );
          },
        ),
      ),
    );
  }
}

You would obviously have the EmailThing class inside a separate folder and not inside the main.dart. Anyways, we have now a list with generated items that is of the type EmailThing, this class has two fields, an email field and a field for the title. We can pass these field values into our email program.

flutter listview with items listing (each containing a email address and title)

You click on one of the List items but nothing will happen, expect some logging. This is where I will put the function to call the launch function, which will open up the email program. Add the launch function like this:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FileIdea Example',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: MyHomePage(title: 'FileIdea example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class EmailThing {
  late String title;
  late String email;

  EmailThing(title, email) {
    this.title = title;
    this.email = email;
  }
}

class _MyHomePageState extends State<MyHomePage> {
  String? encodeQueryParameters(Map<String, String> params) {
    return params.entries
        .map((e) =>
            '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
        .join('&');
  }

  @override
  Widget build(BuildContext context) {
    final stuff = List<EmailThing>.generate(10,
        (i) => new EmailThing("thing ${i + 1}", "somerandomdomain$i@info.com"));

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ListView.builder(
          itemCount: stuff.length,
          itemBuilder: (BuildContext context, int index) {
            final Uri emailLaunchUri = Uri(
              scheme: 'mailto',
              path: stuff[index].email,
              query: encodeQueryParameters(
                  <String, String>{'subject': stuff[index].title}),
            );

            return ListTile(
              onTap: () => {launch(emailLaunchUri.toString())},
              tileColor: Colors.green[200],
              title: Text('${stuff[index].title}'),
            );
          },
        ),
      ),
    );
  }
}

So at the row 71-82 is where the important things happen. We will first encode our query parameters such as subject so we got the characters displayed correctly, then we will call on the launch function which will trigger the email program, in this case Gmail, to open up like this:

open email app from flutter image with address and title

You can add body also if you would want that so you already have a message written for you!

            final Uri emailLaunchUri = Uri(
              scheme: 'mailto',
              path: stuff[index].email,
              query: encodeQueryParameters(<String, String>{
                'subject': stuff[index].title,
                'body': "Hey, this is FileIdea, how are you?"
              }),
            );
open email app from flutter image where we have the text in the body

And there you have it!

Flutter Expandable ListView with data from Subcollection

I found a really cool and smart solution for showing a ListView that is expandable with its data coming from its subcollection in Firestore. Very dynamic and works very well! This is all thanks to Rainer Wittmann and Jobel on Stackoverflow, and I’ve been updating it once more to adjust to the latest Fluter and Firestore versions. Let’s get to it.

Before starting I just wanna tell the dependencies and what versions I am running since they are updating this stuff constantly and changing everything so you have to modify it so much keep up with all the versions.

I am using flutter 2.2.2 with these relevant dependencies for firestore.

firebase_core: ^1.3.0
cloud_firestore: ^2.2.2  

First of all, you would want a collection, let us name it projects, and in it a subcollection called items using Firestore. We are going to use the field we name Title for projects and inside the Items subcollection we are going to have a field called ItemName. We will then declare a class called ProjectList with returns a Widget which displays the data in a ListView with the help of StreamBuilder.

So first of, let’s say you have an application like this with Firebase Initialized with nothing in it other than a Text inside the MyHomePage:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatefulWidget {
  // Create the initialization Future outside of `build`:
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  /// The future is part of the state of our widget. We should not call `initializeApp`
  /// directly inside [build].
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          print("err!");
          return Container();
        }

        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done) {
          return MyApp();
        }

        // Otherwise, show something whilst waiting for initialization to complete
        return Container();
      },
    );
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FileIdea Expandable ListView',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'FileIdea Expandable ListView'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          // Here we take the value from the MyHomePage object that was created by
          // the App.build method, and use it to set our appbar title.
          title: Text(widget.title),
        ),
        body: Text("My widget place"));
  }
}

Then we will add this highlighted code to our project/app:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatefulWidget {
  // Create the initialization Future outside of `build`:
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  /// The future is part of the state of our widget. We should not call `initializeApp`
  /// directly inside [build].
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          print("err!");
          return Container();
        }

        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done) {
          return MyApp();
        }

        // Otherwise, show something whilst waiting for initialization to complete
        return Container();
      },
    );
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FileIdea Expandable ListView',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'FileIdea Expandable ListView'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: ProjectList(),
    );
  }
}

class ExpansionTileList extends StatelessWidget {
  final List<DocumentSnapshot> documents;
  final FirebaseFirestore firestore = FirebaseFirestore.instance;

  ExpansionTileList({required this.documents});

  List<Widget> _getChildren() {
    List<Widget> children = [];
    documents.forEach((doc) {
      children.add(
        ProjectsExpansionTile(
          name: doc['Title'],
          projectKey: doc.id,
          firestore: firestore,
        ),
      );
    });
    return children;
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: _getChildren(),
    );
  }
}

class ProjectList extends StatelessWidget {
  ProjectList();

  final FirebaseFirestore firestore = FirebaseFirestore.instance;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: firestore.collection('projects').snapshots(),
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        if (!snapshot.hasData) return const Text('Loading...');
        //final int projectsCount = snapshot.data.documents.length;
        List<DocumentSnapshot> documents = snapshot.data!.docs;
        return ExpansionTileList(
          documents: documents,
        );
      },
    );
  }
}

class ProjectsExpansionTile extends StatelessWidget {
  ProjectsExpansionTile(
      {required this.projectKey, required this.name, required this.firestore});

  final String projectKey;
  final String name;
  final FirebaseFirestore firestore;

  @override
  Widget build(BuildContext context) {
    PageStorageKey _projectKey = PageStorageKey('$projectKey');

    return ExpansionTile(
      key: _projectKey,
      title: Text(
        name,
        style: TextStyle(fontSize: 28.0),
      ),
      children: <Widget>[
        StreamBuilder(
            stream: firestore
                .collection('projects')
                .doc(projectKey)
                .collection('items')
                .snapshots(),
            builder:
                (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
              if (!snapshot.hasData) return const Text('Loading...');
              //final int surveysCount = snapshot.data.documents.length;
              List<DocumentSnapshot> documents = snapshot.data!.docs;

              List<Widget> surveysList = [];
              documents.forEach((doc) {
                PageStorageKey _surveyKey = new PageStorageKey('${doc.id}');

                surveysList.add(ListTile(
                  key: _surveyKey,
                  title: Text(doc['ItemName']),
                ));
              });
              return Column(children: surveysList);
            })
      ],
    );
  }
}

And there we go. We have a list showing documents from a collection called projects like this

It is expandable, so we can click on the arrow to the right and it will add new documents from the subcollection called Items in real-time since we are using snapshots().