Flutter Basics: Dart(3)

3-1 Classes

  • Dart is an OO language

3-1-1 Ways to initiate objects

  • literal
1
2
String text = 'initial';
final list = [1,2,3];
  • invoke a constructor

constructor with initial value of fields

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void main(List<String> arguments) {
  MyClass myClass = MyClass('initial');
  //"new" keyword is redundant
}

class MyClass {
  String someField;
  //constructor
  //this refers to the current instance itself
  MyClass(this.someField);
}

constructor with named parameters

1
2
3
4
5
6
7
8
void main(List<String> arguments) {
  MyClass myClass = MyClass(someField: 'initial');
}

class MyClass {
  String someField;
  MyClass({required this.someField});
}

constructor with lazy initialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void main(List<String> arguments) {
  MyClass myClass = MyClass(firstName: 'Cherry', lastName: 'Lu');
}

class MyClass {
  //promise to initiate the variable later
  late String name;
  
  MyClass({required String firstName, 
  required String lastName}){
    name = '$firstName $lastName';
  }
}

★constructor with a initializer list

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void main(List<String> arguments) {
  MyClass myClass = MyClass(firstName: 'Cherry', lastName: 'Lu');
}

class MyClass {
  String name;
  
  //initializer list is a , separated list of
  //expressions that can access constructor 
  //parameters and can assign to instance fields, 
  //even final instance fields. 
  MyClass({required String firstName, 
  required String lastName}) : name = '$firstName $lastName';
}

★const constructor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void main(List<String> arguments) {
  MyClass myClass1 = const MyClass('Cherry');
  MyClass myClass2 = const MyClass('Cherry');
  //const constructor make sure the object can be instantiated only once
  //so the result will be true.
  print(myClass1 == myClass2);
}

class MyClass {
  //fields referred from a const constructor must be final
  final String name;
  const MyClass(this.name);
}

3-1-2 Methods in a class

Nothing special~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void main(List<String> arguments) {
  MyClass myClass = MyClass('Cherry');
  print(myClass.getName());
}

class MyClass {
  String name;
  MyClass(this.name);

  bool getName() {
    return name.contains('C');
  }
}

3-1-3 Static Members

1
2
3
4
5
6
7
8
9
void main(List<String> arguments) {
  int myAge = MyClass.age;
  MyClass.method();
}

class MyClass {
  static const age = 20;//static field
  static void method(){ }//static method
}

3-1-4 Private Members

  • _means private access modifier
  • No class private fields but only package private fields in dart
  • files are packages
  • use import to import other files(packages)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void main(List<String> arguments) {
  MyClass(1,2);
  final myClass = MyClass.namedConstructor(public: 1, private: 2);
  myClass._private; //This is fine! There's no class private fields!
}

class MyClass {
  int public;
  int _private;

  MyClass(this.public, this._private);
  MyClass.namedConstructor({ //multiple constructors
    required this.public, //named parameters
    required int private, 
  }) : _private = private;
  //★have to initiate here because 
  //named parameter can't start with '_'
}
  • private constructor
1
2
3
4
class NonInstantiable {
//unaccessible from outside of the class
NonInstantiable._();
NonInstantiable._namedConstructor();
  • private method
1
void _privateMethod(){ }

3-1-5 Properties

  • Properties are like some nice-looking methods which are doing lighter work than normal methods.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    void main(List<String> arguments) {
      final user = User(firstName: 'Cherry', lastName: 'Lu', email: 'c@l.com');
      final user2 = User(firstName: 'Cherry', lastName: 'Lu', email: 'whdgsuf');
    
      print(user.fullName + ' ' + user.email); //Cherry Lu c@l.com
      print(user2.fullName + ' ' + user2.email); //Cherry Lu Invalid Email
    }
    
    class User {
      final String firstName;
      final String lastName;
      String? _email; //nullable & private
    
      User({
        required this.firstName,
        required this.lastName,
        required String email,
      }) {
        //can't use initializer list
        //because this.email is not a actual field but a property
        //'this.email' refers to the 'set' property
        this.email = email;
      }
    
      String get fullName => '$firstName $lastName';
      //'??' means if null
      String get email => _email ?? 'Invalid Email';
    
      set email(String value) {
        if (value.contains('@')) {
          _email = value;
        } else {
          _email = null;
        }
      }
    }
    

3-1-6 Equality

  • Referential Equality: E.g. Objects that are instantiated with a constant constructor will be referentially equal to each other [link: const constructor].
  • Value Equality: Defined by overriding equality(the behavior of equal operator ==), depending on if the specific fields are equal.
    • cmd+.->generate equality use extension dart data class generator
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    @override
    bool operator ==(Object other) {
      //this: operator on the left side, other: right side
      //identical: check referential equality
      if (identical(this, other)) return true;
    
      return other is User && //type check
            other.firstName == firstName && //fields check
            other.lastName == lastName;
    }
    
    //for quick look up on map
    @override
    int get hashCode => firstName.hashCode ^ lastName.hashCode;
    

3-2 Inheritance

3-2-1 Extending and Overriding

  • In dart, every single class extends Object
  • Extending a class
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    class User {
      final String _firstName;
      final String _lastName;
    
      User(
        this._firstName,this._lastName,
      );
      String get fullName => '$_firstName $_lastName';
      void signOut() {print('Signing out...');}
    }
    
    //Need to declaring a zero argument constructor in 'User',
    //or declaring a constructor in Admin that explicitly invokes a constructor in 'User'
    class Admin extends User {
      //As long as the super call is correct,
      //it'll be fine to do anything with the sub constructor
      Admin(String firstName, String lastName) : super(firstName, lastName);
      ...
    }
    
  • Overriding properties
    1
    2
    
    @override
    String get fullName => 'Admin: ${super.fullName}';
    
  • Overriding methods
    1
    2
    3
    4
    5
    
    @override
    void signOut() {
      print('Performing admin-specific sign out steps');
      super.signOut(); //how to be precautious at missing the super call??
    }
    
  • ★Guarantee super method to be called
    • pubspec.yaml: add library meta to regular dependencies
    • Super Class: add annotation @mustCallSuper to the super method
    1
    2
    
    @mustCallSuper
    void signOut() {print('Signing out...');}
    
  • ★PolyMorphism
    • Use as to cast child type to its super type.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    void main(List<String> arguments) {
      final admin = Admin('Cherry', 'Lu');
      //By casting, the special fields defined only in Admin class
      //can not be accessed anymore.
      final user = admin as User;  
      //But, call the common methods will still invoke the one in the child class.
      print(user.fullName);//Admin: Cherry Lu
      print(user is Admin);//true
      print(user is! Admin);//false
    
      if (user is Admin) {
        //Special fields defined only in Admin class will be accessible here!!!!
      }
    }
    

3-2-2 Factory Constructors

  • Dart has a factory keyword for factory constructors
  • Factory is able to return current/subclass instance
  • Normal constructor is only able to return an instance of the current class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    //inside of the previous User class
    factory User.admin() {
      return Admin('Cherry', 'Lu');
    }
    ...
    //call from the main function
    void main(List<String> arguments) {
      final user = User.admin();
    }
    

3-2-3 Abstract Classes

  • Use abstract keyword to prevent a class from being instantiated.
  • Abstract class is allowed to have constructors.
  • An abstract class cannot be instantiated directly(with new), but can be instantiated from its subclasses.
  • Abstract classes can have abstract methods.
  • Once an abstract class has any abstract methods or properties, its subclasses must override all of them.

3-2-4 Interfaces

  • In dart, any kinds of classes can be implemented as an interface. Therefore there's no such a Interface keyword.
  • By implementing a regular class, we are actually implementing the implicit interface --the overall class members without implementations, of the class.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    class User {
      final String _name;
    
      User(this._name);
      String get upperName => _name.toUpperCase();
      void loggedIn() {
        print('Logged in.');
      }
    }
    
    //Ignore the original implementations and always need to provide new ones
    class Admin implements User {
      @override
      // TODO: implement _name
      // Every fields must have a getter
      String get _name => throw UnimplementedError();
      @override
      void loggedIn() {
      // TODO: implement loggedIn
      }
      @override
      // TODO: implement upperName
      String get upperName => throw UnimplementedError();
    }
    
  • The implementation classes can always be cast into the 'interface class'
    1
    
    Admin() as User;//unnecessary
    
  • Although implementing a regular class is allowed, one should always implement an abstract class.

3-3 Generics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
abstract class DataReader<T> {//generics in class
  T readData(); //generic return type
  void myMethod(T arg); //generic arguments
}

class IntegerDataReader implements DataReader<int> {
  @override
  int readData() {
    return 1234;
  }

  @override
  void myMethod(int arg) {
    print(arg);
  }
}

3-4 Mixins

  • Dart does not support multiple inheritance.
  • Mixin type of class allows other classes to access it's members by using the 'with' keyword, without building an 'is-a' inheritance relationship with that class.
  • Mixin type is like it allows copying of its members.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
mixin Action {
  void run() {
    print("I'm running");
  }
}

class LivingThing {}
class Thing {}

//Both animals and roberts can run
//An animal is a living thing but not an action
class Animal extends LivingThing with Action {}
//A robert is a thing but not an action
class Robert extends Thing with Action {
  @override
  void run() {
    print("I'm fully charged!");
    super.run(); //Even override and super works here
  }
}

Call method:

1
2
3
4
5
6
7
void main(List<String> arguments) {
  final rabbit = new Animal();
  final alphaRun = new Robert();

  rabbit.run(); //I'm running
  alphaRun.run(); //I'm fully charged!¥nI'm running
}

3-5 Extensions

  • Extension methods helps us to add functionalities to the classes that we don't own

extends method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void main(List<String> arguments) {
  final x = 'hello'.duplicate();
  print(x);//hellohello
}
//Extension on String class
extension StringDuplication on String {
  String duplicate() {
    return this + this; //this = current instance of String
  }
}

extends property

1
2
3
4
5
6
7
8
void main(List<String> arguments) {
  final x = 'hello'.duplicated;
  print(x);//hellohello
}

extension StringDuplication on String {
  String get duplicated => this + this;
}

3-6 Access Package Private Class

  • Use part keyword to link package private classes from different files(packages).

file1.dart: has a part named file2:

1
2
3
4
5
part 'file2.dart'
//...
class _privatePackageClass {
  //...
}

file2.dart: is a part of file1.dart

1
part of 'file1.dart'

now it's possible to access file1's private class from file2.

  • part of must be the only directive in a part

3-7 Data Classes

3-7-1 Immutable Data Classes

  • In many cases, directly changing values of instance fields is not allowed.
  • Generating copyWith method to make a copy of the original instance for safe updating.
  • Simply generate DataClass or only copyWith method by pressing cmd + .
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@immutable
class User {
  final String name;

  const User({
    required this.name,
  });

  User copyWith({
    String? name, //could be null(no updating)
  }) {
    return User(
      name: name ?? this.name, //if null keep the original value
    );
  }
}

output:

1
2
3
4
5
6
7
8
void main(List<String> arguments) {
  User user1 = const User(name: 'Cherry');
  User userUpdated = user1.copyWith(name: 'Lu');

  print(user1.name); //Cherry
  print(userUpdated.name); //Lu
  print(user1 == userUpdated); //false
}

3-7-2 Freezed Data Classes

  • Freezed: Code generation for immutable classes that has a simple syntax/API without compromising on the features. (make 3-7-1 code more readable?)
  • Dependencies
    • dev_dependencies: freezed, build_runner
    • dependencies: freezed_annotation
  • How to Use
    • Create the target class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @freezed //annotation
    class User with _$User {
      //only called by freezed generated class
      const User._();
    
      const factory User({
        String? name, //nullable
      }) = _User; // redirect to the generated class
    }
    
    • Designate a part to hold the generated class
    1
    
    part 'target_file_name.dart';
    
    • Run the command
    1
    
    dart run build_runner build --delete-conflicting-outputs
    
    • Use it
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    void main(List<String> arguments) {
      const userOrigin = User(name: 'Cherry');
      final userCopy = userOrigin.copyWith(name: 'CC');
      final userNull = userOrigin.copyWith(name: null);
    
      print('userOrigin: ${userOrigin.name}');//Cherry
      print('userCopy: ${userCopy.name}');//CC
      print('userNull: ${userNull.name}');//null
    
      print('Are they the same? : ${userOrigin == userCopy}');//false
    }
    
  • ★Create snippets
    • Press F1 => Choose Configure User Snippets => Choose dart.json
    • Define snippets like fdataclass for create a data class

3-8 Freezed Union Classes

  • Original Union: Checking what the type of an object actually is may not be exhaustive
    1
    2
    3
    4
    5
    6
    
      //what-already-know: object is an instance of superClass which is extended by subClass1, subClass2...
      if (object is subClass1) {
        //
      } else if (object is subClass2) {
        //...
      } //there could be subClass3,4...and we just don't know them yet
    
  • Freezed Union: Has the previous exhausted checks built-in for multiple extended classes.
  • How to Use
    • Create the target class(snippet: funion)
    1
    2
    3
    4
    5
    6
    7
    8
    
    @freezed //annotation
    class Result with _$Result {
      const Result._();
      //In the case of Union , there are always multiple classes so every factory for them must a name
      //otherwise it is going to create a DataClass
      const factory Result.success(int value) = _Success;//an union case
      const factory Result.failure(String errorMessage) = _Failure;
    }
    
    • Designate a part to hold the generated class
    1
    
    part 'target_file_name.dart';
    
    • Run the command
    1
    
    dart run build_runner build --delete-conflicting-outputs
    
    • How to use
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    void main(List<String> arguments) {
      //This result is actually returned by our api so
      //we don't know what exactly it is.
      const actualResult = Result.success(200);
    
      //'when' clause help us to exhaustively check every single
      //possible type of the result.
      print(actualResult.when(success: (value) {
        return 'Successfully called API. Response code is: $value';
      }, failure: (errorMessage) {
        return 'An error occurred. ErrorMessage: $errorMessage';
      }));
    
      //console output: Successfully called API. Response code is: 200
    }
    
    • Use maybeWhen to ignore certain cases
    1
    2
    3
    4
    5
    
    print(actualResult.maybeWhen(
      orElse: () => '', //when it's not a Failure do this
      failure: (errorMessage) {
      return 'An error occurred. ErrorMessage: $errorMessage';
    }));
    
    • Use map/maybeMap to pass whole generated objects to the functions
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    //TResult map<TResult extends Object?>({required TResult Function
    //(_Success) success, required TResult Function(_Failure) failure})
    print(actualResult.map(
          success: (_success) { //
          return 'Successfully called API. Response code is: ${_success.value}';
        }, failure: (_failure) {
          return 'An error occurred. ErrorMessage: ${_failure.errorMessage}';
        },
      ),
    );
    

3-9 Error Handling

  • try-catch-finally
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    //catch all
    void main(List<String> arguments) {
      try {
        final myInt = int.parse('abcdefg');
      } catch (e) {
        print(e);
      } finally {
        //always runs
      }
    }
    
    //partially catch
    void main(List<String> arguments) {
      try {
        final myInt = int.parse('abcdefg');
      } on FormatException catch (e) {
        print(e); 
        //the 'catch (e)' part can be removed if there's nothing to do here
      }
    }
    
  • dart can throw anything as an exception to crash the app but don't do this!
    1
    2
    3
    4
    5
    6
    7
    
    ...
    throw 'a fake exception' //don't do this!
    throw MyCustomException; //correct.
    
    ...
    class MyCustomException implements Exception { }
    class MyCustomError extends Error { }
    

3-9 Asynchrony

3-9-1 Basics

  • By doing async calls within an method, the method itself becomes asynchronous as well, which must return an Future object as its result.
  • How to use
    • async-await (recommended)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    //need 'http' library
    Future<void> main(List<String> arguments) async {
      try {
        final result = await Client().get(
        Uri.parse('https://jsonplaceholder.typicode.com/posts'),
      );
        print(result.body);
      } catch(e) {
        //..
      }
    }
    
    • then
    1
    2
    3
    4
    5
    6
    7
    8
    
    void main(List<String> arguments) {
      Client()
          .get(
            Uri.parse('https://jsonplaceholder.typicode.com/posts'),
          )
          .then((response) => print(response.body))
          .catchError((e) => print(e));
    }
    

3-9-2 Stream

  • listen to periodic stream
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Future<void> main(List<String> arguments) async {
      final periodStream = Stream.periodic(const Duration(seconds: 1));
      final subscription = periodStream.listen((event) {
        print('A second has passed');
      });
    
      await Future.delayed(const Duration(seconds: 3));
      subscription.cancel(); //await for 3s to before cancel
    }
    
  • Stream Generators and Operators
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    Future<void> main(List<String> arguments) async {
      createMessageStream().listen((event) => print(event));
      //console output:
      //Message1...
      //Message2...
      //Message3...
    }
    
    //async* is an async generator, which returns Streams
    //Streams are like one level above Future cause
    //Future handles an one time async event but Streams handles multiple ones
    Stream<String> createMessageStream() async* {
      //cannot use return because it terminates the method.
      //yield is able to return something without terminating the execution
      yield 'Message1...';//should match the return type String
      await Future.delayed(const Duration(seconds: 1));
      yield 'Message2...';
      await Future.delayed(const Duration(seconds: 1));
      yield 'Message3...';
      await Future.delayed(const Duration(seconds: 1));
    }
    
  • Stream is like a collection of async objects that alive in the streams over a period of time.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    //1.use map on it(just like using it on a collection)
    createMessageStream()
        .map((message) => message.toUpperCase())
        .listen((event) => print(event));
    // then the previous out put will become to:
    //MESSAGE1...
    //MESSAGE2...
    //MESSAGE3...
    
    //2.use filter on it
    createMessageStream()
        .map((message) => message.toUpperCase())
        .where((message) => message.contains('1'))
        .listen((event) => print(event));
    //only Message1... will be printed!