Flutter entry series - flutter air safety

What is empty security?? Under what circumstances is it safe to use empty space? What version?

Since Flutter 2, Flutter has enabled null security by default in the configuration. By incorporating null checking into the type system, these errors can be caught during the development process, so as to prevent the crash caused by the reproduction environment.

Personal idea: when declaring variables and functions, it is determined that they cannot be empty, so the problem of empty exception can be rejected during operation. Null Exception

1, What is empty security

Today, air security has become a common topic. At present, mainstream programming languages such as Kotlin, Swift and Rust have their own support for air security. Dart has supported null security since version 2.12. Through null security, developers can effectively avoid null error crash. Null security can be said to be an important supplement to dart language. It further enhances the type system by distinguishing nullable types from non nullable types.

1. Benefits of introducing air safety

1. The null value reference error in the original runtime can be changed into the analysis error in editing; [identify problems in advance]

2. Enhance the robustness of the program and effectively avoid the crash caused by Null;

3. Follow the development trend of Dart and Flutter and leave no holes for the subsequent iteration of the program;

2. Minimum necessary knowledge of air safety

1. Principle of air safety

2. Changes of Dart type system before and after the introduction of air safety

3. Nullable (?) Use of type

4. Use of late [not in the impression]

5. Null assertion operator (!) Use of

3. Principle of air safety

Dart's air security support is based on the following three core principles:

1. Non nullable by default: unless you explicitly declare the variable as nullable, it must be a non nullable type;

2. Progressive migration: you can freely choose when to migrate and how much code to migrate;

3. Completely reliable: Dart's empty security is very reliable, which means that many optimizations are included during compilation;

4. If the type system infers that a variable is not empty, it will never be empty. When you completely migrate the entire project and its dependencies to empty security, you will enjoy all the advantages of soundness - fewer bugs, smaller binaries, and faster execution.

4. Changes of Dart type system before and after the introduction of air safety

Before introducing null security, Dart's type system changed as follows:

This means that before, all types can be Null, that is, Null types are regarded as subclasses of all types.

After introducing empty security:

It can be seen that the biggest change is to separate the Null type, which means that Null is no longer a subtype of other types, so a type conversion error will be reported when passing a Null value to a variable of non Null type.

Tip: you will often see?.,!.,, in the project of Flutter or Dart using empty security A large number of applications of late, so what are they and how to use them? See the analysis below

5. Controllable (?) Use of type

Can we pass? Follow the type to indicate that the variable or parameter following it can accept Null:

class CommonModel {
  String? firstName; //Nullable member variable
  int getNameLen(String? lastName /*Nullable parameter*/) {
    int firstLen = firstName?.length ?? 0;
    int lastLen = lastName?.length ?? 0;
    return firstLen + lastLen;
  }
}

For nullable variables or parameters, you need to use Dart's void avoidance operator To access, otherwise a compilation error will be thrown.

When empty security is enabled in the program, the member variable of the class cannot be empty by default. Therefore, for a non empty member variable, its initialization method needs to be specified:

class CommonModel {
  List names=[];//Initialize on definition
  final List colors;//Initialize in constructor
  late List urls;//Delay initialization
  CommonModel(this.colors);
  ...
}

6. Use of late

If you cannot initialize at the time of definition and want to avoid using, Then delayed initialization can help you. Through the variable modified by late, developers can choose the initialization time by themselves, and can not use?. when using this variable.

late List urls;//Delay initialization

setUrls(List urls){
   this.urls=urls; //Initialize
}

int getUrlLen(){
   return urls.length;
}

Although delayed initialization can bring us some convenience in coding, it will lead to the problem of null exception if it is not used properly. Therefore, when using it, we must ensure the order of assignment and access, and do not reverse it.

late use paradigm

Some variables initialized in the initState method of State in fluent are more suitable for delayed initialization. Because the initState method is executed first in the Widget life cycle, the variables initialized in it can not only ensure the convenience of use, but also prevent null exceptions after being modified by late.

class _SpeakPageState extends 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();
        }
      });
  }

}

7. Null assertion operator (!) Use of

When we exclude the possibility that variables or parameters can be null, we can pass! To tell the compiler that this nullable variable or parameter is not nullable, which is particularly useful when we pass method parameters or nullable parameters to a non nullable input parameter:

As mentioned above, if you want to use nullable variables to declare variables, you must upgrade the Dart SDK to 2.12.0.  

zfz:component zhangfengzhou$ flutter --version
Flutter 2.0.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision adc687823a (8 months ago) • 2021-04-16 09:40:20 -0700
Engine • revision b09f014e96
Tools • Dart 2.12.3
environment:
  sdk: ">=2.12.0 <3.0.0" #Opening an empty security sdk refers to the version of Dart
  #sdk: ">=2.7.0 <3.0.0"  #Turn off empty safety
void main() {
   //print("hello world");
   //isEmpty("");
   String? name ="lisi";
   name =null;
   print(name!.length);
}

When the name is declared, it is nullable. The assignment is non empty, and then it is changed to null. Therefore, if it is null, an exception will be thrown

Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:

 - package:hi_base

For solutions, see https://dart.dev/go/unsound-null-safety

Process finished with exit code 254

besides,! Another common use:

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

Used here to indicate negation. The above code is equivalent to:

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

2, Air safety adaptation

1. Enable empty security

Flutter 2 enables empty security by default, so items created through flutter 2 have been checked for empty security. In addition, small partners can also view your version of Flutter SDK through the following command:

flutter doctor

zfz:component zhangfengzhou$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.0.5, on macOS 12.0.1 21A559 darwin-x64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[!] Android Studio (version 2020.3)
    ✗ Unable to find bundled Java version.
[✓] IntelliJ IDEA Ultimate Edition (version 2019.3.1)
[✓] VS Code (version 1.55.2)
[✓] Connected device (2 available)

! Doctor found issues in 1 category.
zfz:component zhangfengzhou$ 

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

environment:
  sdk: ">=2.12.0 <3.0.0" //SDK > = 2.12.0 indicates that the empty security check is enabled

Tip: once the project starts the empty security check, your code, including the three-party plug-ins that the project depends on, must support empty security, otherwise it cannot be compiled normally.

If you want to turn off the empty security check, you can adjust the support scope of the SDK to less than 2.12.0, such as:

environment: 
  sdk: ">=2.7.0 <3.0.0" 

2. Air safety adaptation

When you turn on empty security, and then run the project, you will see a lot of error reports, and then locate the error files. There are generally three types of files that need to be modified: 1 Custom Widget 2 Data Model 3 Single case

1. The air security adaptation of custom widgets can be divided into two cases: 1 Air safety adaptation of Widget 2 Air safety adaptation of state

1. Wiget's air safety adaptation

For a custom Widget, whether it is a control of the page or the whole page, some properties are usually defined for the Widget. During air safety adaptation, the attributes shall be classified as follows:

1. Nullable attribute: pass the "empty" attribute? Modify

2. Non nullable attribute: set the default value in the constructor or modify it through required

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})

Tip: if @ required , is used in the construction method, it needs to be changed to required.  

2. Air safety adaptation of state

The air security adaptation of State is mainly classified according to whether its member variables can be empty:

1. Nullable variables: pass the "empty" function? Modify

2. Non null variables: the following two methods can be used for adaptation: 1 Initialization on definition 2 Use late to modify as a delay variable

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

3. Air safety adaptation skills of data Model

1. Model with command constructor 2 Model with named factory constructor

Model with command constructor

Before air safety adaptation

///Travel page model
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;
  }
}

Before adaptation, we must first negotiate with the server. Those fields in the model can be empty, and those fields will be distributed. For this case, if the totalCount field is bound to be distributed and the resultList field is not guaranteed to be distributed, we can adapt it in this way: [pay attention to these details]

After air safety adaptation

///Travel page model
class TravelItemModel {
  late int totalCount; 
  List<TravelItem>? resultList;

  //Named construction method
  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;
  }
}
  • For the fields that must be distributed, we modify them as delayed initialization fields through late to facilitate access
  • For fields that cannot be guaranteed to be distributed, we can pass the? Modify it as an nullable variable

Model with named factory constructor

The data model named factory constructor is also one of the more common data models.

Before adaptation

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']
    );
  }
}

Models with named factory constructors usually need to have their own constructors. Constructors usually use optional parameters. Therefore, when adapting, first clarify which fields must not be empty and which fields can be empty. After confirmation, you can adapt as follows:

After adaptation:

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 have a return value. Similar to static functions, member variables and methods cannot be accessed
  factory CommonModel.fromJson(Map<String, dynamic> json) {
    return CommonModel(
      icon: json['icon'],
      title: json['title'],
      url: json['url'],
      statusBarColor: json['statusBarColor'],
      hideAppBar: json['hideAppBar']
    );
  }
}
  • For nullable fields, click OK? Modify
  • For non nullable fields, you need to add the required modifier in front of the corresponding field in the construction method to indicate that this parameter is a required parameter

4. Single case air safety adaptation skills

Singleton is the most widely used design pattern in the development of fluent. How can singleton adapt to air safety?

Before adaptation:

///Cache management class
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;
  }
}

After adaptation:

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;
  }
}

There are two main points for core adaptation:

  • Because it is a singleton of lazy mode, the singleton instance is set to nullable
  • Because singletons will be created when null in getInstance, they will be converted to non null when returning instance

5. Air safety adaptation of plug-ins

Air security adaptation of third-party plug-ins

At present, the mainstream plug-ins on Dart's official plug-in platform have carried out empty security support one after another. If your project has enabled empty security, all plug-ins used must also support empty security, otherwise they 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_screen

When you encounter this problem, you can check it on Dart's official plug-in platform flutter_splash_screen Whether the plug-in has a version that supports empty security. If the plug-in supports empty security, the plug-in platform will mark it safe:

3. Common problems of air safety adaptation

Problems and Solutions

type 'Null' is not a subtype of type 'xxx'

Problem description

After running the APP, the console outputs the above log

Problem analysis

The main reason for this problem is that a null value is passed to a parameter that cannot be null. It is common when using model, such as:

type 'Null' is not a subtype of type 'String'.
type 'Null' is not a subtype of type 'bool'.

Solution

  • If the console outputs the specific number of error lines, you can jump to the specific code line to solve it
  • If the console does not have a specific number of lines of code, you can make a breakpoint under the catch or catchrror node of the code by debug ging, and then check the specific error information in the catch, such as:

Breakpoint:

Locate the specific number of error lines through error:

The above figure shows the error reported when converting json data into model during network request. In this way, it is located that the error occurred in:

  • package:flutter_ trip/model/travel_ model. Line 272 of dart file
  • Looking at line 272, it is found that the isWaterMarked field is a field that cannot be empty, and a null value is received
  • The solution is to add an nullable decoration to isWaterMarked
late bool isWaterMarked;
//Change to
bool? isWaterMarked;

Keywords: Flutter security

Added by mullz on Tue, 25 Jan 2022 10:47:47 +0200