Home Services Work About Blog Contact Let's Talk
Blog / Flutter Apps
Flutter Apps

Flutter + Microsoft Graph API: Access Calendar, Mail & Contacts in Your Mobile App

Why Connect Flutter to the Microsoft Graph?

The Microsoft Graph API is the unified gateway to nearly every piece of data and intelligence in the Microsoft 365 ecosystem — calendar events, emails, contacts, Teams messages, SharePoint files, user profiles, and much more. For enterprise mobile apps built with Flutter, integrating with Microsoft Graph means your users can access their Outlook calendar, read and send emails, look up colleagues, and interact with SharePoint content directly from a native mobile experience — without Microsoft ever releasing a first-party Flutter SDK.

In our Flutter Microsoft 365 projects, we've seen the Graph integration become the differentiating feature. A field service app that shows technicians their next appointment from Outlook, pre-populated with the customer's contact details from the People API, eliminates context-switching between the custom app and native Outlook. A procurement app that sends approval emails directly through the user's Exchange mailbox keeps all communication in one auditable thread. These are enterprise-grade capabilities that become straightforward once you have a reliable Graph client in Dart.

This guide targets Flutter developers who have an app working and need to connect it to Microsoft 365 data. We assume familiarity with Flutter state management and Dart async/await patterns, but we'll cover the Graph-specific concepts from first principles. We're also writing this in May 2025 ahead of the Flutter 3.35 stable release — everything here is compatible with Flutter 3.22 and later.

Setting Up MSAL Authentication in Flutter

Microsoft Authentication Library (MSAL) handles the OAuth 2.0 authorization code flow with PKCE — the correct authentication pattern for mobile apps accessing the Microsoft Graph. The msal_flutter package wraps the native MSAL libraries for both iOS and Android, giving you a consistent Dart API. Before writing any Flutter code, you need to register your application in the Azure portal under Entra ID (formerly Azure AD).

YAML — pubspec.yaml dependencies
dependencies:
  flutter:
    sdk: flutter
  msal_flutter: ^1.2.0      # MSAL authentication
  http: ^1.2.1              # Graph HTTP calls
  flutter_secure_storage: ^9.0.0  # Secure token storage
  shared_preferences: ^2.2.3      # Cache metadata
  intl: ^0.19.0             # Date formatting for calendar
  connectivity_plus: ^6.0.3 # Network state detection
JSON — MSAL Configuration (assets/auth_config.json)
{
  "client_id": "YOUR_AZURE_APP_CLIENT_ID",
  "authority_type": "AAD",
  "authority": "https://login.microsoftonline.com/YOUR_TENANT_ID",
  "redirect_uri": "msauth://com.yourcompany.yourapp/YOUR_BASE64_SIGNATURE",
  "cache_location": "keystore",
  "logging": {
    "pii_enabled": false,
    "log_level": "WARNING"
  }
}

On Android, add the MSAL redirect URI scheme to your AndroidManifest.xml as a BrowserTabActivity intent filter. On iOS, add the LSApplicationQueriesSchemes and CFBundleURLSchemes entries to your Info.plist. The MSAL package README covers these platform-specific steps in detail — we always recommend running flutter pub run msal_flutter:setup to automate the configuration. Request the scopes you need at login time: User.Read, Calendars.ReadWrite, Mail.ReadWrite, and Contacts.Read are a reasonable starting set for a productivity app.

Building a Typed Graph HTTP Client in Dart

Rather than making raw HTTP calls to Graph endpoints throughout your app, we always build a typed GraphClient class that handles token acquisition, request construction, error handling, and response deserialisation in one place. This pattern pays enormous dividends in maintainability — when Graph API versions change or token refresh logic needs updating, there is exactly one file to modify.

Dart — GraphClient typed HTTP client
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:msal_flutter/msal_flutter.dart';

class GraphClient {
  static const String _baseUrl = 'https://graph.microsoft.com/v1.0';
  final PublicClientApplication _msal;

  GraphClient(this._msal);

  Future<String> _getAccessToken() async {
    try {
      // Try silent token acquisition first
      final result = await _msal.acquireTokenSilent(
        scopes: ['User.Read', 'Calendars.ReadWrite', 'Mail.ReadWrite', 'Contacts.Read'],
      );
      return result.accessToken;
    } on MsalUserInteractionRequiredException {
      // Fall back to interactive login
      final result = await _msal.acquireToken(
        scopes: ['User.Read', 'Calendars.ReadWrite', 'Mail.ReadWrite', 'Contacts.Read'],
      );
      return result.accessToken;
    }
  }

  Future<Map<String, dynamic>> get(String endpoint) async {
    final token = await _getAccessToken();
    final response = await http.get(
      Uri.parse('$_baseUrl$endpoint'),
      headers: {
        'Authorization': 'Bearer $token',
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    );
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as Map<String, dynamic>;
    }
    throw GraphException(response.statusCode, response.body);
  }

  Future<void> post(String endpoint, Map<String, dynamic> body) async {
    final token = await _getAccessToken();
    final response = await http.post(
      Uri.parse('$_baseUrl$endpoint'),
      headers: { 'Authorization': 'Bearer $token', 'Content-Type': 'application/json' },
      body: jsonEncode(body),
    );
    if (response.statusCode != 202 && response.statusCode != 201 && response.statusCode != 200) {
      throw GraphException(response.statusCode, response.body);
    }
  }
}

class GraphException implements Exception {
  final int statusCode;
  final String message;
  GraphException(this.statusCode, this.message);
}

Reading and Displaying Outlook Calendar Events

Calendar events are available at /me/events or /me/calendarView — the latter is preferred for showing events in a date range because it expands recurring event series into individual instances. For a typical mobile calendar view showing the next 7 days, use /me/calendarView with startDateTime and endDateTime query parameters. Request only the fields you need using the $select parameter to reduce payload size on mobile networks.

Dart — CalendarEvent model and fetch method
class CalendarEvent {
  final String id;
  final String subject;
  final DateTime start;
  final DateTime end;
  final String? location;
  final String? bodyPreview;
  final bool isOnlineMeeting;
  final String? onlineMeetingUrl;

  CalendarEvent.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        subject = json['subject'] ?? '(No Title)',
        start = DateTime.parse(json['start']['dateTime']).toLocal(),
        end = DateTime.parse(json['end']['dateTime']).toLocal(),
        location = json['location']?['displayName'],
        bodyPreview = json['bodyPreview'],
        isOnlineMeeting = json['isOnlineMeeting'] ?? false,
        onlineMeetingUrl = json['onlineMeetingUrl'];
}

Future<List<CalendarEvent>> getCalendarEvents(GraphClient client) async {
  final now = DateTime.now().toUtc();
  final weekLater = now.add(const Duration(days: 7));
  final endpoint = '/me/calendarView'
    '?startDateTime=${now.toIso8601String()}'
    '&endDateTime=${weekLater.toIso8601String()}'
    '&\$select=id,subject,start,end,location,bodyPreview,isOnlineMeeting,onlineMeetingUrl'
    '&\$orderby=start/dateTime'
    '&\$top=50';
  final data = await client.get(endpoint);
  return (data['value'] as List)
      .map((e) => CalendarEvent.fromJson(e))
      .toList();
}

Sending Emails Through the Graph Mail API

Sending email through Microsoft Graph on behalf of the authenticated user requires the Mail.Send permission scope. The endpoint is POST /me/sendMail, and the request body follows the Graph message schema. One pattern we use in our apps is a strongly-typed MailMessage builder class that constructs the JSON body — this prevents the typo-prone approach of building nested maps by hand throughout the codebase.

Dart — sendMail request body construction
Future<void> sendApprovalEmail(GraphClient client, {
  required String toEmail,
  required String toName,
  required String subject,
  required String htmlBody,
}) async {
  await client.post('/me/sendMail', {
    'message': {
      'subject': subject,
      'body': {
        'contentType': 'HTML',
        'content': htmlBody,
      },
      'toRecipients': [
        {
          'emailAddress': {
            'address': toEmail,
            'name': toName,
          }
        }
      ],
      'importance': 'Normal',
    },
    'saveToSentItems': true,
  });
}

For reading the user's inbox, use GET /me/messages with $filter to limit to unread messages or specific folders. The $top parameter controls page size — always pair it with $skip or use the @odata.nextLink pattern (covered in the pagination section) to avoid loading hundreds of emails in a single call on a mobile connection.

Fetching Contacts and the People API

The Microsoft Graph exposes two related but distinct endpoints for people data: /me/contacts returns the user's personal Outlook contacts (their address book), and /me/people returns a relevance-ranked list of people the user works with most — drawn from email history, meetings, and org chart proximity. For a mobile app that needs to suggest recipients or display a "My Team" widget, the People API is usually more useful than the raw contacts list.

The People API returns contacts sorted by relevance with a relevanceScore field. You can filter by $search to implement a live contact search — each keystroke sends a new request to /me/people?$search="query"&$select=displayName,emailAddresses,jobTitle,mobilePhone. Debounce the search input to avoid overwhelming the API with requests while the user is typing. In our Flutter apps we use a 350ms debounce timer attached to the search TextField's onChanged callback.

The /me/contacts endpoint is the right choice when you need to create, update, or delete contacts — the People API is read-only. When building a CRM-adjacent feature where field workers can save new customer contacts to their Outlook address book, use a POST to /me/contacts with the contact JSON. This creates the contact in the user's default contacts folder and syncs it to Outlook on all their devices automatically.

Handling Pagination and @odata.nextLink

Every Microsoft Graph collection endpoint that can return more than a handful of items supports OData pagination. When the result set exceeds the page size (controlled by $top, maximum 999 for most endpoints), the response includes an @odata.nextLink property containing the URL for the next page. Ignoring pagination is one of the most common bugs we see in Graph integrations — it works fine in development with small inboxes but fails silently in production when a user has 2,000 emails and the app only shows the first 10.

Dart — Paginated Graph fetch with @odata.nextLink
Future<List<Map<String, dynamic>>> fetchAllPages(
  GraphClient client,
  String initialEndpoint,
) async {
  final allItems = <Map<String, dynamic>>[];
  String? nextUrl = initialEndpoint;

  while (nextUrl != null) {
    final data = await client.get(nextUrl);
    final items = data['value'] as List? ?? [];
    allItems.addAll(items.cast<Map<String, dynamic>>());

    // Follow next page link if present
    nextUrl = data['@odata.nextLink'] as String?;

    // For very large sets, yield between pages to keep UI responsive
    if (nextUrl != null) await Future.delayed(const Duration(milliseconds: 50));
  }

  return allItems;
}

For mobile apps where you want to show data progressively as pages load, use a StreamController to emit each page of results as they arrive rather than waiting for all pages to complete. This gives the user something to see and interact with immediately while the background fetch continues. Pair this with a ListView.builder driven by the stream for a smooth progressive loading experience.

Caching Graph Responses for Offline Resilience

Mobile apps that depend entirely on live Graph API calls will frustrate users the moment they enter a tunnel, a basement car park, or a conference centre with poor Wi-Fi. Caching Graph responses locally gives users access to their calendar and contacts even without connectivity, and dramatically improves perceived performance — local reads are instant compared to a 200–800ms Graph round trip.

Our recommended caching strategy: use flutter_secure_storage for sensitive data (access tokens, email bodies) and shared_preferences or a local SQLite database via sqflite for less sensitive index data (event IDs, contact metadata). Store a cache timestamp alongside each cached response and define a staleness threshold — for calendar events, 15 minutes is reasonable; for contacts, 24 hours is acceptable since they change infrequently.

Implement a cache-first fetch pattern: on app startup, immediately show cached data to the user, then kick off a background refresh from the Graph API. When the fresh data arrives, diff it against the cache and update only the changed items. This prevents the janky blank-screen-then-content experience common in apps that show nothing until the API responds. For calendar events specifically, also listen for changes using Graph change notifications (webhooks) if your architecture supports a backend — this eliminates polling entirely.

Key Takeaways

Use msal_flutter for MSAL authentication and build a centralised typed GraphClient class to handle token acquisition, retries, and error handling in one place.

Use /me/calendarView instead of /me/events for date-range calendar display — it correctly expands recurring event series into individual instances.

Always handle @odata.nextLink pagination — Graph endpoints are page-limited and silent pagination failures are a common production bug.

Prefer the People API (/me/people) over raw contacts for relevance-ranked person search; use /me/contacts only when you need to write contact data.

Cache Graph responses locally using a cache-first pattern — show stale data immediately and refresh in the background to avoid blank screens and improve perceived performance.

AT

Akshara Technologies

Microsoft 365 Development Specialists

With 10+ years building enterprise SharePoint, SPFx, Power Automate, and Flutter solutions for clients across India, USA, UAE, and Australia — we write from production experience, not documentation.

Related Articles

Ready to Build with Microsoft 365?

From SPFx web parts to full intranet portals — Akshara Technologies delivers enterprise-grade Microsoft 365 solutions that actually work.

Start Your Project View Case Studies