avatarConstantin Stan

Summary

The provided web content is an in-depth guide on object-oriented programming (OOP) in Dart, focusing on its application within Flutter projects, and covers concepts such as classes, objects, inheritance, encapsulation, abstraction, and polymorphism.

Abstract

The article titled "Fluttering Dart: OOP" delves into the object-oriented programming features of Dart, emphasizing its use in Flutter app development. It explains how Dart allows for the creation of custom data types through classes, enabling the modeling of objects within programs. The article discusses various types of constructors in Dart, including default, named, redirecting, constant, and factory constructors, and illustrates how classes can be invoked like functions using the call() method. It also explores the concepts of generators for producing sequences of values, encapsulation at the library level, and the distinction between instance and class variables and methods. The text highlights Dart's support for inheritance, mixins for code reuse, and composition, as well as abstraction through abstract classes and interfaces. Finally, it touches on polymorphism and Dart's approach to overcoming the single-thread limitation with futures and isolates, setting the stage for the next part of the series.

Opinions

  • The author of the article, "Fluttering Dart," aims to provide fundamental knowledge and tips & tricks for using Dart effectively in Flutter applications.
  • The article suggests that Dart's mixin-based inheritance is a powerful feature that compensates for the lack of multiple inheritances.
  • Code examples are encouraged to be tried out in DartPad for hands-on learning and experimentation.
  • The use of factory constructors in Dart is presented as a flexible way to manage object creation, allowing for object pooling or the return of cached instances.
  • The author expresses that Dart's approach to encapsulation, which is at the library level rather than the class level, is straightforward and based on a simple rule involving the underscore prefix.
  • The article conveys that Dart's single direct inheritance, combined with mixins, provides a robust system for extending class functionalities and achieving composition.
  • The absence of the final class concept in Dart is noted, implying that any class can be extended.
  • The author emphasizes the importance of abstract classes and interfaces in Dart for defining class behavior and creating flexible and maintainable code structures.
  • The article concludes with an anticipatory note, hinting at the exploration of futures and isolates in the subsequent part of the "Fluttering Dart" series to address Dart's single-threaded nature.

Fluttering Dart

Fluttering Dart: OOP

Classes, Objects, Interfaces, and a lot more

Flutter projects can use both platform-specific and cross-platform code. The latter is written in Dart, and, for building Flutter apps, some basic knowledge of Dart is required.

Fluttering Dart’s goal is to explore fundamental knowledge and unveil tips & tricks of the powerful programming language that brings Flutter to life.

In the previous parts of the series, we went through the Dart built-in data types, functions, operators and control flow statements.

In this part, we’ll discover Dart as the true object-orientated programming language it is.

Some of the code examples can be tried out, and played with, using DartPad.

Classes and Objects

Remember the built-in data types we’ve covered at the beginning of our journey? Classes allow us to define our very own data types! This way we can model objects we need to use in our programs.

A class is a user-defined data type and in the examples, up to this point we’ve already defined some classes, the most memorable probably being the Cat class.

In Dart, every object is an instance of a class and all classes descend from Object. Dart also has a mixin-based inheritance and that comes to aid the lack of multiple inheritances. This inheritance type allows the reuse of multiple class bodies and the existence of exactly one superclass.

Classes define members: functions and data (methods and instance variables). Invoking a method on an object is the act of calling a method. A public method has access to that object’s members.

The dot operator . is used to refer to a variable or method.

We can create an object of a defined class using one of its constructors.

Constructor names can be either the class name ClassName or ClassName.identifier. For example we’ll create a Cat using Cat() or Cat.copyCat() constructors.

Constructors can have arguments to provide necessary values to initialize new objects and are of several types:

  • default — when we don’t declare a constructor, a default constructor that has no arguments and invokes the no-argument constructor in the superclass is provided; note that if you declare a constructor there will be no default constructor and if you extend a class its constructor is not inherited;
class Cat {
  DateTime birthday;
  // default
  // it's here
  // even if
  // you can't see it
}
  • named — used when we need to implement multiple constructors for a class or to provide extra clarity;
class Cat {
  DateTime birthday;
  // named
  Cat.baby() {
    birthday = DateTime.now();
  }
}
  • redirecting — when we just want to redirect certain constructor in the same class; its body is empty with the constructor call appearing after :;
class Cat {
  DateTime birthday;
 
  // main cosntructor
  Cat(this.birthday);
  // delegating to main constructor
  Cat.withBirthday(DateTime birthday) : this(birthday);
}
  • constant — use when we need objects that never change; when calling such constructors we should use the keyword const otherwise we won’t create constants;
class CatTreat {
  static final CatTreat catTreat = const CatTreat(1);
  
  final num quantity;
  // constant
  const CatTreat(this.quantity);
}
  • factory — when implementing a constructor that doesn’t always create a new instance of its class; a factory constructor might return a cached instance, do object pooling, or it might return an instance of a subtype; factory constructors don’t have access to this.
import 'dart:math';
class Cat extends Pet {
  DateTime birthday;
  Cat(this.birthday);
  // delegating to main constructor
  Cat.withBirthday(DateTime birthday) : this(birthday);
}
class Dog extends Pet {
  DateTime birthday;
  Dog(this.birthday);
  // delegating to main constructor
  Dog.withBirthday(DateTime birthday) : this(birthday);
}
// factory
class Pet {
  Pet();
  factory Pet.withBirthday(DateTime birthday) {
    bool isCat = Random.secure().nextBool();
    return isCat?Cats(birthday):Dog(birthday);
  }
}

The Pet factory constructor from above returns a random Cat or a Dog (subclasses of Pet).

Callable classes

Dart classes can also behave like functions (they can be invoked, take arguments and return something).

To enable this, we have to define the call() method inside the class.

class Cat {
  DateTime birthday;
  Cat(this.birthday);
  String call() {
    print('Meow!');
  }
}
void main() {
  var cat = Cat(DateTime.now());
  cat(); 
  // prints
  // Meow!
}

Generators

Generators are used when we need to lazily produce a sequence of values. Dart supports two types of generator functions:

  • a synchronous generator that returns an Iterable object
  • and an asynchronous generator that returns a Stream object

To implement the synchronous generator we mark the function body as sync*, and use the yield statement to return values:

Iterable<Cat> kittens(int toSpawn) sync* {
  int kittenIndex = 0;
  while(kittenIndex < n) {
    kittenIndex++;
    yield Cat.baby();
  }
}

To implement an asynchronous generator we mark the function body as async*, and use the yield statement to return values:

Stream<Cat> kittens(int toSpawn) async* {
  int kittenIndex = 0;
  while(kittenIndex < n) {
    kittenIndex++;
    yield Cat.baby();
  }
}

If we use recursive calls, a performance improvement can be achieved by using yield*:

Iterable<Cat> kittens(int toSpawn) sync* {
  if(toSpawn > 0) {
    yield Cat.baby;
    yield* kittens(toSpawn - 1);
  }
}

Variables

There are two flavors: instance and class variables.

All uninitialized variables have by default the value null. Also, all of the variables that are not final will generate an implicit getter and setter. The final ones, will not generate a setter.

By default, variables are instance variables. If initialized when declared (instead of in a constructor or method), their value is set when the instance is created, which is before the constructor and its initializer list execute.

To create a class variable we’ll use the static keyword. These are useful for class-wide state and constants. They are not initialized until they’re used.

Methods

Methods are functions that provide behavior for an object.

Like in the case of variables, here are also two flavors: instance and class methods.

Instance methods on objects can access instance variables and this.

Static methods (class methods) do not operate on an instance, and thus do not have access to this. They are best used as compile-time constants (for example, passed as a parameter to a constant constructor). We should use top-level functions, instead of static methods, for common or widely used utilities and functionality.

Encapsulation

Dart doesn’t contain keywords for restricting access, like public, protected or private used in Java. The encapsulation happens at library level, not at class level.

There is a simple rule: any identifier (class, class member, top-level function, or variable) that starts with an underscore _ it is private to its library.

Inheritance and composition

Inheritance allows extending a class to a specialized version of that class. As said before all classes inherit from the Object type, just by declaring a class, we extend the Object type. Dart allows single direct inheritance and has special support for mixins, which can be used to extend class functionalities without direct inheritance, simulating multiple inheritances, and reusing code. This is how composition is achieved.

Mixins are a way of reusing a class’s code in multiple class hierarchies. To use a mixin, use the with keyword followed by one or more mixin names. To specify that only certain types can use the mixin — for example, so your mixin can invoke a method that it doesn’t define — use on to specify the required superclass.

There’s no final class, so a class can always be extended.

Abstraction

Abstraction is the process through which we define a class and its essential characteristics, leaving implementation for its subclasses.

To declare an abstract class, we use the abstract keyword. These classes can’t be instantiated and are useful for defining interfaces. Abstract classes can have abstract methods.

There is no interface keyword. The way it works is that every declared class defines an implicit interface containing all instance members of a class and of any interfaces it implements. This means that any class can be implemented by others without extending it.

A class can implement one or more interfaces by using the implements keyword.

Polymorphism

Polymorphism is achieved through inheritance and represents the ability of an object to copy the behavior of another (the int or double are also a num).

We can use the extends to create a subclass and super to refer to the superclass.

Subclasses usually override instance methods, getters, and setters. We can use the @override annotation to indicate that we’re overriding a member.

Dart doesn’t allow overloading. To overcome this we can use the flexible argument definitions (optional and positional).

Overall, Dart provides all of the bells and whistles that we need to use the OOP paradigm.

In the next part of the Fluttering Dart series, we’ll delve into Futures and Isolates to discover how to overcome Dart’s single-thread downside.

Tha(nk|t’)s all!

Programming
Dart
Flutter
Coding
Fluttering Dart
Recommended from ReadMedium