Since Flutter 2, Flutter has been configured with null safety enabled by default. By incorporating null checking into the type system, these errors can be caught during development to prevent crashes caused by a production environment.

What is air safety

Today, air security has become a common topic. At present, mainstream programming languages such as Kotlin, Swift, Rust and so on have their own support for air security. Dart has supported null safety since version 2.12, which allows developers to avoid null error crashes. Null security is an important addition to the Dart language that further enhances the type system by distinguishing between nullable and non-nullable types.

Introduce air safety benefits

  • A null reference error at runtime can be turned into an analysis error at edit time;
  • Enhance the robustness of the program, effectively avoid crashes caused by Null;
  • Follow Dart and Flutter trends and leave no holes for subsequent iterations of the program;

Minimum necessary knowledge of air safety

  • Principles of air safety
  • Dart type system changes before and after the introduction of air safety
  • Void (?) Use of types
  • The use of late
  • The null-value assertion operator (!) The use of

Principles of air safety

Dart’s air safety support is based on three core principles:

  • Non-nullable by default: Unless you explicitly declare a variable to be nullable, it must be of a non-null type;

  • Progressive migration: You are free to choose when and how much code is migrated;

  • Completely reliable: Dart’s space safety is very reliable, meaning that a lot of optimizations are included at compile time,

    If the type system concludes that a variable is not null, it is never null. When you fully migrate your entire project and its dependencies to space security, you enjoy all the benefits of soundness — fewer bugs, smaller binaries, and faster execution.

Dart type system changes before and after the introduction of air safety

Dart’s type system before air safety was introduced looked like this:

This means that previously, all types could be Null, meaning that Nul types were considered subclasses of all types.

After the introduction of air security:

As you can see, the biggest change is the separation of Null types,This means that Null is no longer a subtype of any other type, so passing a Null value to a variable of a non-NULL type will result in a conversion error.

Tip: How often do you see this in a Flutter or Dart project that uses air safety? .,! , a large number of late applications, so what are they and how to use them? See the analysis below

Void (?) Use of types

We can do this by putting? To indicate that a variable or argument following a type accepts Null:

class CommonModel { String? firstName; // nullable member variable getNameLen(String? LastName /* nullable argument */) {int firstLen = firstName? .length ?? 0; int lastLen = lastName? .length ?? 0; return firstLen + lastLen; }}Copy the code

Dart air escape operators are required for nullable variables or parameters to be used? . Or a compilation error will be thrown.

When null-safe is enabled, member variables of a class cannot be nullable by default, so you need to specify the initialization method for a non-empty member variable:

class CommonModel { List names=[]; // Initialize final List colors when defining; // Initialize the late List urls in the constructor; // delay initialization of CommonModel(this.colors); .Copy the code

The use of late

For those who cannot initialize at definition and want to avoid using? .then delayed initialization can help you. A variable modified with late that allows developers to choose when to initialize and use it without using it? .

late List urls; // delay initialization setUrls(List urls){this.urls=urls; } int getUrlLen(){ return urls.length; }Copy the code

Although delayed initialization can bring some convenience to our coding, it will bring empty exception problem if used improperly, so when using it, we must ensure that the order of assignment and access, do not reverse.

Late initialization (LATE) uses the paradigm

Some variables initialized in the initState method of Flutter State are suitable for late initialization. Since initState is the first method to be executed in the Widget life cycle, variables initialized in Flutter State can be easily used after late modification. To prevent empty exceptions, the following uses Flutter from entry to advanced voice search module as an example:

The class _SpeakPageState extends the State < SpeakPage > with SingleTickerProviderStateMixin {String speakTips = 'long press to speak; String speakResult = ''; late Animation<double> animation; late AnimationController controller; @override void initState() { controller = AnimationController( super.initState(); vsync: this, duration: Duration(milliseconds: 1000)); animation = CurvedAnimation(parent: controller, curve: Curves.easeIn) .. addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); }}); }...Copy the code

The null-value assertion operator (!) The use of

When we rule out the nullable possibility of a variable or parameter, we can pass! To tell the compiler that the nullable variable or parameter is not nullable. This is especially useful when passing a method parameter or passing a nullable parameter to a non-nullable input parameter:

  Widget get _listView {
    return ListView(
      children: <Widget>[
        _banner,
        Padding(
          padding: EdgeInsets.fromLTRB(7, 4, 7, 4),
          child: LocalNav(localNavList: localNavList),
        ),
        if (gridNavModel != null)
          Padding(
              padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
              child: GridNav(gridNavModel: gridNavModel!)),
        Padding(
            padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
            child: SubNav(subNavList: subNavList)),
        if (salesBoxModel != null)
          Padding(
              padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
              child: SalesBox(salesBox: salesBoxModel!)),
      ],
    );
  }
Copy the code

The above code is a list that the home page module of Flutter creates dynamically based on whether the data of the gridNavModel and salesBoxModel modules is empty or not, using the null assertion operator while ensuring that the variables are not empty! .

Besides,! Another common use:

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty;
}
Copy the code

The above code is equivalent to:

bool isEmptyList(Object object) { if (! (object is List)) return false; return object.isEmpty; }Copy the code

How does Flutter short fit safely

Step 1: Enable air security

Flutter 2 has air safety enabled by default, so any project created with Flutter 2 already has air safety enabled. You can also check your Flutter SDK version by running the following command:

flutter doctor
Copy the code

So, how to manually turn on and off the gap safety?

Environment: SDK: ">=2.12.0 <3.0.0" // SDK >=2.12.0 indicates that the null security check is enabledCopy the code

Tip: once a project has null-safe checking enabled, your code, including the tripartite plug-ins that the project relies on, must support null-safe or it will not compile properly.

If you want to turn off null security check, you can set the support range of SDK to below 2.12.0, for example:

Environment: the SDK: "> = 2.7.0 < 3.0.0"Copy the code

Step 2: Perform air safety adaptation

After enabling air security, run the project and you will see many errors. Locate the error file and apply the techniques described in this chapter. The files need to be sorted first:

  • Custom Widgets (containing the Flutter pages you created)

  • Data Model (Model)

  • The singleton

There are two types of air security adaptation for custom widgets:

  • Null security adaptation of widgets
  • State air security adaptation

Null security adaptation of widgets

For custom widgets, either a control on the page or the entire page, there are usually some properties defined for the Widget. In air safety adaptation, the attributes should be classified as follows:

  • Nullable property: Pass?A modified
  • Non-null property: Set the default value in the constructor or passrequiredA modified
class WebView extends StatefulWidget {
  String? url;
  final String? statusBarColor;
  final String? title;
  final bool? hideAppBar;
  final bool backForbid;

  WebView(
      {this.url,
      this.statusBarColor,
      this.title,
      this.hideAppBar,
      this.backForbid = false})
      ...
Copy the code

The above case is the effect of WebView module after air security adaptation.

Tip: If @required is used in the constructor, change it to required.

State air security adaptation

The null-safe adaptation of State is mainly classified according to whether its member variables are nullable:

  • Nullable variable: pass? A modified

  • Non-empty variables: You can adapt them in either of the following ways

    • Class when defined
    • uselateIs a delay variable

The following is the main code effect after State ADAPTS to empty security for reference:

class _TravelPageState extends State<TravelPage> with TickerProviderStateMixin { late TabController _controller; List<TravelTab> tabs = []; // Initialize... @override void initState() { super.initState(); _controller = TabController(length: 0, vsync: this); .Copy the code

Null-safe adaptation of data models

Data Model (Model) Air security adaptation mainly includes the following two situations:

  • A model with a command constructor
  • A model with a named factory constructor

A model with a command constructor

Next, we share the air safety adaptation techniques of the model with command constructors by taking the data model of the brigade module as an example:

Adapter before:

Class TravelItemModel {int totalCount; List<TravelItem> resultList; TravelItemModel.fromJson(Map<String, dynamic> json) { totalCount = json['totalCount']; if (json['resultList'] ! = null) { resultList = new List<TravelItem>(); json['resultList'].forEach((v) { resultList.add(new TravelItem.fromJson(v)); }); } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['totalCount'] = this.totalCount; if (this.resultList ! = null) { data['resultList'] = this.resultList.map((v) => v.toJson()).toList(); } return data; }}Copy the code

Before adaptation, the first thing to do is to negotiate with the server. Those fields in the model can be empty, and those fields will be delivered. For this example, if the totalCount field must be delivered, whereas the resultList field is not guaranteed to be delivered, we can use this method:

After the adapter:

Class TravelItemModel {late int totalCount; List<TravelItem>? resultList; Travelitemmodel.fromjson (Map<String, dynamic> json) {totalCount = json['totalCount']; if (json['resultList'] ! = null) { resultList = new List<TravelItem>.empty(growable: true); json['resultList'].forEach((v) { resultList! .add(new TravelItem.fromJson(v)); }); } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['totalCount'] = this.totalCount; data['resultList'] = this.resultList! .map((v) => v.toJson()).toList(); return data; }}Copy the code
  • For fields that must be delivered, we passlateTo modify fields that are lazily initialized for easy access
  • For fields that cannot be guaranteed to be delivered, we pass?Decorate it as a nullable variable

A model with a named factory constructor

The data model of the named factory constructor is also one of the more common data models. Let’s use the common data model as an example to share the null-safe adaptation techniques of the data model that contains the named factory constructor:

Adapter before:

class CommonModel { final String icon; final String title; final String url; final String statusBarColor; final bool hideAppBar; CommonModel( {this.icon, this.title, this.url, this.statusBarColor, this.hideAppBar}); factory CommonModel.fromJson(Map<String, dynamic> json) { return CommonModel( icon: json['icon'], title: json['title'], url: json['url'], statusBarColor: json['statusBarColor'], hideAppBar: json['hideAppBar'] ); }}Copy the code

A model with a named factory constructor usually needs to have its own constructor. Constructors usually take optional parameters, so the first part of the adaptation should be clear which fields must not be empty, which fields can be empty, and then the following adaptation can be done:

After the adapter:

class CommonModel { final String? icon; final String? title; final String url; final String? statusBarColor; final bool? hideAppBar; CommonModel( {this.icon, this.title, required this.url, this.statusBarColor, this.hideAppBar}); // The named factory constructor must return a value, Json(Map<String, dynamic> json) {return CommonModel(icon: json['icon'], title: json['title'], url: json['url'], statusBarColor: json['statusBarColor'], hideAppBar: json['hideAppBar'] ); }}Copy the code
  • For nullable fields pass?A modified
  • Non-nullable fields need to be added in the constructor before the corresponding fieldrequiredModifier to indicate that this parameter is required

The Flutter singleton ADAPTS to empty safety

Here are some tips on empty security adaptation of cache modules in Flutter advanced practice:

Adapter before:

Class HiCache {SharedPreferences prefs; static HiCache _instance; HiCache._() { init(); } HiCache._pre(SharedPreferences prefs) { this.prefs = prefs; } static Future<HiCache> preInit() async { if (_instance == null) { var prefs = await SharedPreferences.getInstance(); _instance = HiCache._pre(prefs); } return _instance; } static HiCache getInstance() { if (_instance == null) { _instance = HiCache._(); } return _instance; } void init() async { if (prefs == null) { prefs = await SharedPreferences.getInstance(); } } setString(String key, String value) { prefs.setString(key, value); } setDouble(String key, double value) { prefs.setDouble(key, value); } setInt(String key, int value) { prefs.setInt(key, value); } setBool(String key, bool value) { prefs.setBool(key, value); } setStringList(String key, List<String> value) { prefs.setStringList(key, value); } T get<T>(String key) { return prefs? .get(key) ?? null; }}Copy the code

After the adapter:

class HiCache { SharedPreferences? prefs; static HiCache? _instance; HiCache._() { init(); } HiCache._pre(SharedPreferences prefs) { this.prefs = prefs; } static Future<HiCache> preInit() async { if (_instance == null) { var prefs = await SharedPreferences.getInstance(); _instance = HiCache._pre(prefs); } return _instance! ; } static HiCache getInstance() { if (_instance == null) { _instance = HiCache._(); } return _instance! ; } void init() async { if (prefs == null) { prefs = await SharedPreferences.getInstance(); } } setString(String key, String value) { prefs? .setString(key, value); } setDouble(String key, double value) { prefs? .setDouble(key, value); } setInt(String key, int value) { prefs? .setInt(key, value); } setBool(String key, bool value) { prefs? .setBool(key, value); } setStringList(String key, List<String> value) { prefs? .setStringList(key, value); } remove(String key) { prefs? .remove(key); } T? get<T>(String key) { var result = prefs? .get(key); if (result ! = null) { return result as T; } return null; }}Copy the code

There are two main points of core adaptation:

  • Because it is a lazy singleton, the singleton is set to nullable
  • In getInstance, the singleton will be created with null, so it will be converted to non-null when the instance is returned

Three – party plug-in air security adaptation problem

Dart plugins support space safety. If you have space safety enabled in your project, all plugins must support space safety as well. Otherwise, you will fail to compile:

Xcode 's output: ↳ Error: Cannot run with sound null safety, because the following dependencies don't support null safety: - package:flutter_splash_screenCopy the code

If you run into this problem, check the official Dart plugin platform to see if there is a version of flutter_splash_screen that supports empty security. If the plugin supports air safety, the plugin platform will mark it as air safety:

If you are using a plugin that does not support air safety and you must use the plugin, you can turn off air safety checks by using the method described above.

How does my plugin fit air security

The whole process of plug-in adaptation can be divided into three key steps:

  • Turn on air safety
  • Code adaptation: to compile, to compile errors for empty security adaptation
  • Release: Release the adapted code to the plug-in marketplace

Air security adaptation Common Problems

Question list

  • Type ‘Null’ is not a subtype of type ‘XXX’

Problem settlement method

Type ‘Null’ is not a subtype of type ‘XXX’

Problem description

The console outputs the above log after running the APP

Problem analysis

The main cause of this problem is passing a null value to a parameter that cannot be null. This is common when using model, as in:

type 'Null' is not a subtype of type 'String'.
type 'Null' is not a subtype of type 'bool'.
Copy the code

The solution

  • If the console output a specific number of error lines, you can jump to a specific line of code to resolve
  • If the console does not have a specific number of lines of code, you can debug a breakpoint on the catch or catchError node of the code, and then check the specific error message in the catch.

Breakpoint:

Locate the exact number of lines reported with error:

The following figure shows an error reported when converting JSON data to model during network request:

  • package:flutter_trip/model/travel_model.dartLine 272 of the file
  • A null value was received in line 272 for a marked isWaterMarked field
  • The solution is to add controllable modifier to isWaterMarked
late bool isWaterMarked; / / to bool? isWaterMarked;Copy the code