flutter_phone_state 0.5.9

  • Readme
  • Changelog
  • Example
  • Installing
  • 89

Flutter Phone State Plugin #

pub package Coverage Status

A Flutter plugin that makes it easier to make and track phone calls. The core features are:

  1. Initiate a phone call in 1 line of code
  2. await any in-flight phone call
  3. Watch all phone-related events for a single call, or all calls
  4. Track duration of calls, errors, and cancellations

Getting Started #

Install the plugin:

flutter_phone_state: ^0.5.8

Before you start #

Both Android and iOS put restrictions on accessing phone call data. This plugin makes a best-effort attempt to track the complete lifecycle of a phone call, but it's not perfect and has its limitations. Read the Limitations section below for more info.

Initiate a call #

It's recommended that you initiate calls from your app when possible. This gives you the best chance at tracking the call.

// note: this plugin will remove all non-numeric characters from the phone number
final phoneCall = FlutterPhoneState.makePhoneCall("480-555-1234"); 

A PhoneCall object is the source of truth for the call

showCallInfo(PhoneCall phoneCall) {
    print(phoneCall.status); // ringing, dialing, cancelled, error, connecting, connected, timedOut, disconnected 
    print(phoneCall.isComplete); // Whether the call is complete
    print(phoneCall.events); // A list of call events related to this specific call
}

You can read the PhoneCall.events as a stream, and when the call is completed, the plugin will close the stream gracefully. The plugin watches all in-flight calls, and will force any call to timeout eventually.

watchEvents(PhoneCall phoneCall) {
  phoneCall.eventStream.forEach((PhoneCallEvent event) {
    print("Event $event");
  });
  print("Call is complete");
}

Alternatively, you can just wait for the call to complete

waitForCompletion(PhoneCall phoneCall) async {
  await phoneCall.done;
  print("Call is completed");
}

Accessing in-flight calls #

In-flight calls can be accessed like this:

final activeCalls = FutterPhoneState.activeCalls;

Note that activeCalls is an immutable copy of the calls at the moment you called activeCalls. It won't update automatically.

Watching all events #

Instead of focusing on a single call, you can watch all the events. We recommend using
FlutterPhoneState.phoneCallEventStream - because this Stream incorporates our own tracking logic, call timeouts, failures, etc.

_watchAllPhoneCallEvents() {
  FlutterPhoneState.phoneCallEvents.forEach((PhoneCallEvent event) {
    final phoneCall = event.call;
    print("Got an event $event");
  });
  print("That loop ^^ won't end");
}

If you want, you can subscribe to the raw underlying events. Keep in mind that these events are limited.

_watchAllRawEvents() {
  FlutterPhoneState.rawPhoneEvent.forEach((RawPhoneEvent event) {
    final phoneCall = event.call;
    print("Got an event $event");
  });
  print("That loop ^^ won't end");
}

Limitations #

Phone Numbers #

Neither platform gives us phone numbers with call events. This is largely why we recommend initiating the call using the plugin, so you can tie it back to the original number.

And obviously, this means that you'll never get the phone number from an inbound call. Sorry!

Android #

Android doesn't track nested calls. So, once the first call is active, if you receive another call, or make another call (by putting the first on hold), the second call will not be tracked at all.

Also, Android doesn't provide a unique call identifier, so any call events that occur can't be linked together with a platform-assigned id.

How does it work? #

  1. This plugin registers to AppLifecycleState events, and uses those events to determine when an outbound call has been placed vs cancelled.
  2. When possible, the plugin links phone lifecycle events together by the platform-assigned call identifier. (this works on iOS)
  3. The plugin checks the actual lifecycle states - for example, if one call is connected and the plugin gets a dialing event, it's clear that the dialing event must be for a new/different call, and therefore begins tracking it as a new call.

0.5.9 #

  • Updating dependencies

0.5.5 #

  • Removed extension methods and downgraded required version

0.5.4 #

  • Trying to eliminate pub errorse

0.5.3 #

  • Initial release.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_phone_state/extensions_static.dart';
import 'package:flutter_phone_state/flutter_phone_state.dart';

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

///
/// The example app has the ability to initiate a call from within the app; otherwise, it lists all
/// calls with their state
///
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<RawPhoneEvent> _rawEvents;
  List<PhoneCallEvent> _phoneEvents;

  /// The result of the user typing
  String _phoneNumber;

  @override
  void initState() {
    super.initState();
    _phoneEvents = _accumulate(FlutterPhoneState.phoneCallEvents);
    _rawEvents = _accumulate(FlutterPhoneState.rawPhoneEvents);
  }

  List<R> _accumulate<R>(Stream<R> input) {
    final items = <R>[];
    input.forEach((item) {
      if (item != null) {
        setState(() {
          items.add(item);
        });
      }
    });
    return items;
  }

  /// Extracts a list of phone calls from the accumulated events
  Iterable<PhoneCall> get _completedCalls =>
      Map.fromEntries(_phoneEvents.reversed.map((PhoneCallEvent event) {
        return MapEntry(event.call.id, event.call);
      })).values.where((c) => c.isComplete).toList();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Phone Call State Example App'),
        ),
        body: ListView(
          padding: EdgeInsets.all(10),
          children: [
            Row(children: [
              Flexible(
                  flex: 1,
                  child: TextField(
                    onChanged: (v) => _phoneNumber = v,
                    decoration: InputDecoration(labelText: "Phone number"),
                  )),
              MaterialButton(
                onPressed: () => _initiateCall(),
                child: Text("Make Call", style: TextStyle(color: Colors.white)),
                color: Colors.blue,
              ),
            ]),
            verticalSpace,
            _title("Current Calls"),
            for (final call in FlutterPhoneState.activeCalls)
              _CallCard(phoneCall: call),
            if (FlutterPhoneState.activeCalls.isEmpty)
              Center(child: Text("No Active Calls")),
            _title("Call History"),
            for (final call in _completedCalls)
              _CallCard(
                phoneCall: call,
              ),
            if (_completedCalls.isEmpty)
              Center(child: Text("No Completed Calls")),
            verticalSpace,
            _title("Raw Event History"),
            if (_rawEvents.isNotEmpty)
              Padding(
                padding: EdgeInsets.all(10),
                child: Table(
                  children: [
                    TableRow(children: [
                      Text(
                        "id",
                        style: listHeaderStyle,
                        maxLines: 1,
                      ),
                      Text("number", style: listHeaderStyle),
                      Text("event", style: listHeaderStyle),
                    ]),
                    for (final event in _rawEvents)
                      TableRow(children: [
                        _cell(truncate(event.id, 8)),
                        _cell(event.phoneNumber),
                        _cell(value(event.type)),
                      ]),
                  ],
                ),
              ),
            if (_rawEvents.isEmpty) Center(child: Text("No Raw Events")),
          ],
        ),
      ),
    );
  }

  Widget _cell(text) {
    return Padding(
        padding: EdgeInsets.all(5),
        child: Text(
          text?.toString() ?? '-',
          maxLines: 1,
        ));
  }

  Widget _title(text) {
    return Padding(
        padding: EdgeInsets.only(bottom: 10, top: 5),
        child: Text(text?.toString() ?? '-', maxLines: 1, style: headerStyle));
  }

  _initiateCall() {
    if (_phoneNumber?.isNotEmpty == true) {
      setState(() {
        FlutterPhoneState.startPhoneCall(_phoneNumber);
      });
    }
  }
}

class _CallCard extends StatelessWidget {
  final PhoneCall phoneCall;

  const _CallCard({Key key, this.phoneCall}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
          dense: true,
          leading: Icon(
              phoneCall.isOutbound ? Icons.arrow_upward : Icons.arrow_downward),
          title: Text(
            "+${phoneCall.phoneNumber ?? "Unknown number"}: ${value(phoneCall.status)}",
            overflow: TextOverflow.visible,
          ),
          subtitle: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (phoneCall.id?.isNotEmpty == true)
                Text("id: ${truncate(phoneCall.id, 12)}"),
              for (final event in phoneCall.events)
                Text(
                  "- ${value(event.status) ?? "-"}",
                  maxLines: 1,
                ),
            ],
          ),
          trailing: FutureBuilder<PhoneCall>(
            builder: (context, snap) {
              if (snap.hasData && snap.data?.isComplete == true) {
                return Text("${phoneCall.duration?.inSeconds ?? '?'}s");
              } else {
                return CircularProgressIndicator();
              }
            },
            future: Future.value(phoneCall.done),
          )),
    );
  }
}

const headerStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold);
const listHeaderStyle = TextStyle(fontWeight: FontWeight.bold);
const verticalSpace = SizedBox(height: 10);

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_phone_state: ^0.5.9

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:flutter_phone_state/flutter_phone_state.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
77
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
89
Learn more about scoring.

We analyzed this package on Mar 27, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.5.0 <3.0.0
flutter 0.0.0
logging ^0.11.4 0.11.4
stream_transform ^1.1.0 1.2.0
url_launcher ^5.4.2 5.4.2
uuid ^2.0.4 2.0.4
Transitive dependencies
charcode 1.1.3
collection 1.14.11 1.14.12
convert 2.1.1
crypto 2.1.4
flutter_web_plugins 0.0.0
meta 1.1.8
plugin_platform_interface 1.0.2
sky_engine 0.0.99
typed_data 1.1.6
url_launcher_macos 0.0.1+4
url_launcher_platform_interface 1.0.6
url_launcher_web 0.1.1+1
vector_math 2.0.8
Dev dependencies
flutter_test