Using an OEM debugger

If you are exclusively writing Flutter apps with Dart code and not using platform-specific libraries, or otherwise accessing platform-specific features, you can debug your code using your IDE’s debugger. Only the first section of this guide, Debugging Dart code, is relevant for you.

If you’re writing a platform-specific plugin or using platform-specific libraries written in Swift, ObjectiveC, Java, or Kotlin, you can debug that portion of your code using Xcode (for iOS) or Android Gradle (for Android). This guide shows you how you can connect two debuggers to your Dart app, one for Dart, and one for the OEM code.

Debugging Dart code

Use your IDE for standard Dart debugging. These instructions describe Android Studio, but you can use your preferred IDE with the Flutter and Dart plugins installed and configured.

Dart debugger

  • Open your project in Android Studio. If you don’t have a project yet, create one using the instructions in Test drive.

  • Simultaneously bring up the Debug pane and run the app in the Console view by clicking the bug icon (<img src=/flutter_website/assets/images/docs/testing/debugging/oem/debug-run.png alt=’Debug-run icon’>).

    The first time you launch the app is the slowest. You should see the Debug pane appear at the bottom of the window that looks something like the following:

    <img src=/flutter_website/assets/images/docs/testing/debugging/oem/debug-pane.png alt=’Debug pane’>{:width=”100%”}

    You can configure where the debug pane appears, or even tear it off to its own window using the gear to the right in the Debug pane bar. This is true for any inspector in Android Studio.

  • Add a breakpoint on the counter++ line.

  • In the app, click the + button (FloatingActionButton, or FAB, for short) to increment the counter. The app pauses.

  • The following screenshot shows:

    • Breakpoint in the edit pane.
    • State of the app in the debug pane, when paused at the breakpoint.
    • this variable expanded to display its values.

    <img src=/flutter_website/assets/images/docs/testing/debugging/oem/debug-pane-action.png alt=’App status when hitting the set breakpoint’>{:width=”100%”}

You can step in, out, and over Dart statements, hot reload or resume the app, and use the debugger in the same way you’d use any debugger. The 5: Debug button toggles display of the debug pane.

Flutter inspector

There are two other features provided by the Flutter plugin that you might find useful. The Flutter inspector is a tool for visualizing and exploring the Flutter widget tree and helps you:

  • Understand existing layouts
  • Diagnose layout issues

Toggle display of the inspector using the vertical button to the right of the Android Studio window.

<img src=/flutter_website/assets/images/docs/testing/debugging/oem/flutter-inspector.png alt=’Flutter inspector’>

Flutter outline

The Flutter Outline displays the build method in visual form. Note that this might be different than the widget tree for the build method. Toggle display of the outline using the vertical button to the right of the AS window.

<img src=/flutter_website/assets/images/docs/testing/debugging/oem/flutter-outline.png alt=’screenshot showing the Flutter inspector’>{:width=”100%”}

The rest of this guide shows how to set up your environment to debug OEM code. As you’d expect, the process works differently for iOS and Android.

Debugging with Android Gradle (Android)

In order to debug OEM Android code, you need an app that contains OEM Android code. In this section, you’ll learn how to connect two debuggers to your app: 1) the Dart debugger and, 2) the Android Gradle debugger.

  • Create a basic Flutter app.

  • Replace lib/main.dart with the following example code from the url_launcher package:

// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'URL Launcher',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'URL Launcher'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  Future<void> _launched;

  Future<void> _launchInBrowser(String url) async {
    if (await canLaunch(url)) {
      await launch(url, forceSafariVC: false, forceWebView: false);
    } else {
      throw 'Could not launch $url';
    }
  }

  Future<void> _launchInWebViewOrVC(String url) async {
    if (await canLaunch(url)) {
      await launch(url, forceSafariVC: true, forceWebView: true);
    } else {
      throw 'Could not launch $url';
    }
  }

  Widget _launchStatus(BuildContext context, AsyncSnapshot<void> snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else {
      return Text('');
    }
  }

  @override
  Widget build(BuildContext context) {
    String toLaunch = 'https://flutter.dev';
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(16.0),
              child: Text(toLaunch),
            ),
            ElevatedButton(
              onPressed: () => setState(() {
                    _launched = _launchInBrowser(toLaunch);
                  }),
              child: Text('Launch in browser'),
            ),
            Padding(padding: EdgeInsets.all(16.0)),
            ElevatedButton(
              onPressed: () => setState(() {
                    _launched = _launchInWebViewOrVC(toLaunch);
                  }),
              child: Text('Launch in app'),
            ),
            Padding(padding: EdgeInsets.all(16.0)),
            FutureBuilder<void>(future: _launched, builder: _launchStatus),
          ],
        ),
      ),
    );
  }
}
  • Add the url_launcher dependency to the pubspec file, and run flutter pub get:
name: flutter_app
description: A new Flutter application.
version: 1.0.0+1

dependencies:
  flutter:
    sdk: flutter

  url_launcher: ^3.0.3
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  • Click the debug icon (<img src=/flutter_website/assets/images/docs/testing/debugging/oem/debug-run.png alt=’Debug-run icon’>) to simultaneously bring up the Debug pane and launch the app. Wait for the app to launch on the device, and for the debug pane to indicate Connected. (This can take a minute the first time but is faster for subsequent launches.) The app contains two buttons: 1) Launch in browser opens flutter.dev in your phone’s default browser and 2) Launch in app opens flutter.dev within your app.

    <img src=/flutter_website/assets/images/docs/testing/debugging/oem/launch-flutter-dev.png alt=’screenshot containing two buttons for opening flutter.dev’>

  • Click the Attach debugger to Android process button ( <img src=/flutter_website/assets/images/docs/testing/debugging/oem/attach-process-button.png alt=’looks like a rectangle superimposed with a tiny green bug’> )

  • From the process dialog, you should see an entry for each connected device. Select show all processes to display available processes for each device.

  • Choose the process you want to attach to. In this case, it’s the com.google.clickcount (or com.company.app_name) process for the Motorola Moto G.

    <img src=/flutter_website/assets/images/docs/testing/debugging/oem/choose-process-dialog.png alt=’screenshot containing two buttons for opening flutter.dev’>{:width=”100%”}

  • In the debug pane, you should now see a tab for Android Debugger.

  • In the project pane, expand

    app_name > android > app > src > main > java > io.flutter plugins

    . Double click GeneratedProjectRegistrant to open the Java code in the edit pane.

Both the Dart and OEM debuggers are interacting with the same process. User either, or both, to set breakpoints, examine stack, resume execution… In other words, debug!

<img src=/flutter_website/assets/images/docs/testing/debugging/oem/dart-debugger.png alt=’screenshot of Android Studio in the Dart debug pane.’>{:width=”100%”}

The Dart debug pane with two breakpoints set in `lib/main.dart`

<img src=/flutter_website/assets/images/docs/testing/debugging/oem/android-debugger.png alt=’screenshot of Android Studio in the Android debug pane.’>{:width=”100%”}

The Android debug pane with one breakpoint set in `GeneratedPluginRegistrant.java`. Toggle between the debuggers by clicking the appropriate debugger in the Debug pane's banner.

Debugging with Xcode (iOS)

In order to debug OEM iOS code, you need an app that contains OEM iOS code. In this section, you’ll learn how to connect two debuggers to your app: 1) the Dart debugger and, 2) the Xcode debugger.

[PENDING]

Resources

The following resources have more information on debugging Flutter, iOS, and Android:

Flutter

Android

You can find the following debugging resources on developer.android.com.

iOS

You can find the following debugging resources on developer.apple.com.