36 How to Optimize Development and Debugging Efficiency Through Toolchains

36 How to Optimize Development and Debugging Efficiency Through Toolchains #

Hello, I’m Chen Hang. Today, let’s talk about how to debug a Flutter App.

Software development is usually an iterative and spiraling process. During the iterations, we inevitably deal with bugs frequently, especially in collaborative projects where we not only need to fix our own bugs but also help others resolve theirs.

The process of fixing bugs not only helps us eliminate hidden dangers in our code but also enables us to get familiar with the project faster. Therefore, mastering the skill of debugging is especially important.

In Flutter, debugging code mainly involves logging, breakpoint debugging, and layout debugging. So, in this article, I will provide you with a detailed introduction to code debugging in Flutter focused on these three topics.

Let’s start by looking at how to debug application code through logging.

Output Log #

In order to track and record the running status of an application, we usually output logs during development, which means using the print function to print relevant contextual information to the console. Through these logs, we can locate potential problems in the code.

In many previous articles, we have used the print function extensively to output information during the execution of the application. However, using print to print information involves I/O operations and consumes a significant amount of system resources. At the same time, this output data may expose the details of the app’s execution, so we need to disable these outputs when publishing the official version.

When it comes to the operation method, you might think of commenting out all the print statements before releasing the version and uncommenting them later when debugging is needed. However, this method is undoubtedly tedious and time-consuming. So, does Flutter provide us with a better way?

To enable log debugging functionality based on different runtime environments, we can use debugPrint provided by Flutter instead of print. The debugPrint function also prints messages to the console, but unlike print, it provides the ability to customize printing behavior. In other words, we can assign a function declaration to debugPrint to customize the printing behavior.

For example, in the following code, we define debugPrint as an empty function body, which achieves the functionality of disabling printing with just one click:

debugPrint = (String message, {int wrapWidth}) {};// Empty implementation

In Flutter, we can use different main files to represent the entry points in different environments. For example, in the 34th article “How to Understand Flutter’s Compilation Modes?”, we used main.dart and main-dev.dart to separate the production environment and the development environment. Similarly, we can define different log printing behaviors for the production environment and the development environment through main.dart and main-dev.dart.

In the example below, we define debugPrint as an empty implementation for the production environment and as synchronous data printing for the development environment:

// main.dart
void main() {
  // Set debugPrint to an empty implementation, so it does nothing
  debugPrint = (String message, {int wrapWidth}) {};
  runApp(MyApp()); 
}

// main-dev.dart
void main() async {
  // Set debugPrint to synchronous data printing
  debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronously(message, wrapWidth: wrapWidth);
  runApp(MyApp());
}

As you can see, in the code implementation, as long as we replace all print statements with debugPrint, we can meet the logging requirements in the development environment and ensure that the execution information of the application is not accidentally printed in the production environment.

Breakpoint Debugging #

While logging is convenient, it is very inconvenient if you want to obtain more detailed or finer-grained context information. At this time, we need a more flexible debugging method, which is breakpoint debugging. Breakpoint debugging allows the code to pause at the target statement and execute the subsequent code statements one by one, helping us to focus on the detailed changes of all variable values in the code execution context in real time.

Android Studio provides breakpoint debugging, and the methods for debugging Flutter applications and debugging native Android code are exactly the same. It can be divided into three steps: marking breakpoints, debugging the application, and viewing information.

Next, let’s take the Flutter default counter application template as an example to observe the changes in the _counter value in the code and experience the whole process of breakpoint debugging.

First is marking breakpoints. Since we want to observe the changes of the _counter value, it is ideal to add breakpoints to show the latest _counter value on the interface. Therefore, we can click the mouse on the right side of the line number to add a breakpoint at the position shown by the initialized Text widget.

In the example below, in order to observe whether the _counter is normal when it is equal to 20, we also set a conditional breakpoint _counter==20, so that the debugger will only pause when the counter button is clicked for the 20th time:

Figure 1 Marking breakpoints

After adding breakpoints, a circular breakpoint marker will appear next to the corresponding line number, and the entire line of code will be highlighted. At this point, the breakpoint is added. Of course, we can also add multiple breakpoints at the same time to better observe the execution process of the code.

Then it is debugging the application. Unlike the previous method of running by clicking the run button, this time we need to click the bug icon on the toolbar to start the app in debugging mode, as shown in the following figure:

Figure 2 Debugging the app

After the debugger initializes, our program starts. Since we set the breakpoint at _counter=20, when the counter button is clicked for the 20th time, the code will reach the breakpoint position and automatically enter the Debug view mode.

Figure 3 Debug view mode

As shown in the figure, I divide the Debug view mode into four areas: Area A controls the debugging tools, Area B is the step debugging tool, Area C is the frame debugging window, and Area D is the variable viewing window.

The buttons in Area A are mainly used to control the execution status of the debugging:

Figure 4 Buttons in Area A

  • For example, we can click the Continue button to let the program continue running, click the Stop button to stop the program, click the Restart button to restart the program, or click the Pause button while the program is running normally to pause the program.
  • For example, we can click the Edit Breakpoints button to edit breakpoint information, or click the Disable Breakpoints button to disable breakpoints.

The buttons in Area B are mainly used to control the stepping of breakpoints:

Figure 5 Buttons in Area B

  • For example, we can click the Step Over button to execute the program step by step (but without entering the method body), click the Step Into or Force Step Into button to execute the program statement by statement, and even click the Run to Cursor button to execute the program to the cursor (equivalent to creating a temporary breakpoint).
  • For example, when we think that the method body where the breakpoint is located no longer needs to be executed, we can click the Step Out button to let the program execute the currently entered method immediately, so as to return to the next line of the method call.
  • For example, we can click the Evaluate Expression button to modify the value of any variable through assignment or expression. As shown in the figure below, we use the expression _counter+=100 to update the counter to 120:

Figure 6 Evaluating expressions

Area C indicates the function execution stack contained in the current breakpoint, and Area D displays the variables corresponding to the function frames in its stack.

In this example, the breakpoint is set in the build method of the _MyHomePageState class, so D area displays the variable information included in the build method context (such as _counter, _widget, this, _element, etc.). If we want to switch to other functions in the build method execution stack of _MyHomePageState, such as StatefulElement.build, to view the variable information of the relevant context, we only need to click the corresponding method name in C area.

Figure 7 Switching function execution stack

It can be seen that Android Studio provides rich Flutter debugging capabilities. We can adjust the tracing step length more flexibly through the combination of these basic steps, observe the program execution status, and find bugs in the code.

Layout Debugging #

By using breakpoints, we can view the values of variables related to the execution context and make further judgments based on logic in the debugging panel of Android Studio to determine the steps of tracking execution. However, in most cases, when we use Flutter, our goal is to achieve visual functionality, and visual debugging cannot be easily done through the Debug view mode panel.

In the previous article, we have greatly shortened the time from code writing to interface running with the hot reload mechanism provided by Flutter, which allows us to quickly discover obvious problems between the code and the target interface. However, if we want to quickly discover more subtle problems in the interface, such as alignment and margins, we need to use the Debug Painting, which is a visual debugging tool.

Debug Painting can clearly display the layout boundaries of each control element in the form of auxiliary lines, so we can quickly find out where the layout problem occurs based on the auxiliary lines. Debug Painting is easy to enable; we just need to set the debugPaintSizeEnabled variable to true. As shown below, we enable Debug Painting in the main function:

import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled = true;      // enables Debug Painting
  runApp(new MyApp());
}

After running the code, the execution effect of the app on iPhone X is as follows:

Figure 8 Debug Painting runtime effect

As can be seen, each control element in the counter example has been surrounded by ruler auxiliary lines.

Auxiliary lines provide basic widget visualization capabilities. With auxiliary lines, we can perceive whether there are alignment or padding issues in the interface, but we cannot obtain layout information, such as margin information between the widget and the parent view, widget width and height information, etc.

If we want to obtain widget visualization information (such as layout information, rendering information, etc.) to solve rendering problems, we need to use the more powerful Flutter Inspector. Flutter Inspector provides a powerful visual means for detailed layout data of control elements to help us diagnose layout problems.

To use Flutter Inspector, we need to go back to Android Studio and launch Flutter Inspector through the “Open DevTools” button on the toolbar:

Figure 9 Flutter Inspector launch button

Then, Android Studio will open the browser and display the widget tree structure of the counter example in the panel. As can be seen, the widget tree structure displayed by Flutter Inspector corresponds one-to-one to the implemented widget hierarchy in the code.

Figure 10 Flutter Inspector schematic diagram

Our app runs on iPhone X with a resolution of 375*812. Next, we will use the layout information of the Column component as an example to illustrate the specific usage of Flutter Inspector by confirming that it is centered in the horizontal direction and fills the remaining space of the parent widget in the vertical direction.

To confirm that the Column fills the remaining space of its parent widget in the vertical direction, we first need to determine the information of another child widget of its parent widget, which is the AppBar. We click on the AppBar control in the left pane of Flutter Inspector, and the specific visual information of the AppBar is displayed on the right.

As can be seen, the AppBar control has a left margin and top margin of 0; width of 375 and height of 100:

Figure 11 AppBar in Flutter Inspector

Then, we update the selected widget on the left pane of Flutter Inspector to Column, and the specific visual information of the Column is also updated on the right, such as layout direction, alignment mode, rendering information, and its two child widgets - Text.

As can be seen, the Column widget has a left margin of 38.5 and top margin of 0; width of 298 and height of 712:

Figure 12 Column in Flutter Inspector

Based on the above data, we can conclude:

  • The right margin of the Column = Parent widget width (i.e. 375, the width of iPhone X) - Column left margin (i.e. 38.5) - Column width (i.e. 298) = 38.5. Therefore, the left and right margins are equal, indicating that the Column is centered in the horizontal direction.
  • The height of the Column = Parent widget height (i.e. 812, the height of iPhone X) - AppBar top margin (i.e. 0) - AppBar height (i.e. 100) - Column top margin (i.e. 0) = 712.0. Therefore, the Column completely fills the remaining space of the parent widget after excluding the AppBar in the vertical direction.

Therefore, the layout behavior of the Column is completely as expected.

Summary #

Okay, that’s all for today’s sharing. Let’s summarize the main contents.

Firstly, I taught you how to implement the ability to customize logging output. Flutter provides the debugPrint function, which is a print function that can be overridden. We can define different logging behaviors for the production and development environments to meet the logging needs during development while ensuring that the execution information of logging in the release period is not accidentally printed.

Next, I introduced the Flutter debugging feature provided by Android Studio. Using the counter variable in the counter project as an example, I explained specific debugging methods.

Finally, we learned about Flutter’s layout debugging capability, which includes defining auxiliary lines through Debug Painting and using visualization tools like Flutter Inspector to diagnose layout issues more accurately.

When writing code, it is inevitable to encounter bugs, and debugging is needed when they occur. Debugging code essentially involves a process of narrowing down the scope of the problem, so the basic idea for troubleshooting is binary search.

The so-called binary debugging method refers to combining a stable and reproducible characteristic (such as a crash, the value of a certain variable, or the occurrence of a certain phenomenon) with a method that can divide the problem into two halves (such as breakpoints, assert, or logging). By combining these two techniques iteratively, we can continuously divide the range where the problem may occur in half (such as being able to determine that the code causing the problem occurs before a breakpoint). Using the binary search method, we can quickly narrow down the scope of the problem, thus improving the efficiency of debugging.

Thought Question #

Finally, I have a thought question for you.

Please change the logging behavior of debugPrint in the production environment to write to a log file. There will be a total of 5 log files (0-4), each of which cannot exceed 2MB, but can be written cyclically. If a log file is full, it should move to the next log file, clearing the current log file before writing to it again.

Feel free to leave a comment in the comment section and share your thoughts. I will be waiting for you in the next article! Thank you for listening, and please feel free to share this article with more friends to read together.