Dart Language (Part 2)

 

Streams

A Stream in Dart is a way to handle a sequence of asynchronous events. Instead of waiting for a single value, a stream allows you to work with multiple values over time. It handles asynchronous events over time, useful for data that arrives incrementally, like user inputs or network responses. 

After creation, events like data or errors are added to stream, and subscribers listen to these events. The stream emits events until it is closed, signaling the end of its lifecycle and notifying listeners that no more events will be added.

Streams are categorized into 2 types based on how they handle listeners :

  • Single-Subscription Streams:

    • These streams can have only one subscriber at a time. Once a listener subscribes, no additional listeners can be added. They are typically used for one-time data sources, such as reading from a file or making a single HTTP request.
  • Broadcast Streams:

    • These streams allow multiple listeners to subscribe concurrently. Each listener receives the same stream of events independently. Broadcast streams are ideal for scenarios where multiple subscribers need to react to the same set of events, such as real-time updates or live notifications.

StreamController

StreamController is a class that provides a way to create a stream and control its events. You can add data, handle errors, and close the stream. It’s useful when you need to generate events dynamically or manage stream-related tasks more directly.

We can pass various types of data through a stream, including basic data types like int, double, String, and bool, as well as custom objects, collections such as List and Map, and even null if the stream is nullable.

NOTE: By default, StreamController creates a single-subscription stream. If you need a broadcast stream, you have to specify this explicitly when creating stream.

Example] Below we create a single subscription stream and listen to various types of events like when data is passed, errors added or stream closed.


import 'dart:async';

void main() {

// Create a single-subscription StreamController
final controller = StreamController<int>();

// Get the stream from the controller
final stream = controller.stream;

// Listen to the stream
stream.listen((data) {
print('Received: $data');
});

// Add data to the stream
controller.add(1);
controller.add(2);

// Close the controller
controller.close();
}


import 'dart:async';

void main() {

// Create a single-subscription StreamController
final controller = StreamController<int>();

// Access the stream from the controller
final stream = controller.stream;

// Listen to the stream for data events
stream.listen(
(data) {
print('Data received: $data');
},
onError: (error) {
print('Error received: $error');
},
onDone: () {
print('Stream closed.');
},
);

// Add data events
controller.add(1);
controller.add(2);
controller.add(3);

// Add an error event
controller.addError('Something went wrong');

// Add more data events
controller.add(4);
controller.add(5);

// Close the controller
controller.close();
}

Example] Below we create a broadcast stream and listen to various types of events like when data is passed, errors added or stream closed.


import 'dart:async';

void main() {

// Create a broadcast StreamController
final controller = StreamController<int>.broadcast();

// First listener
controller.stream.listen(
(data) {
print('Listener 1 received: $data');
},
onError: (error) {
print('Listener 1 received error: $error');
},
);

// Second listener
controller.stream.listen(
(data) {
print('Listener 2 received: $data');
},
onError: (error) {
print('Listener 2 received error: $error');
},
);

// Add data events
controller.add(10);
controller.add(20);
controller.add(30);

// Add an error event
controller.addError('An error occurred');

// Add more data events after the error
controller.add(40);
controller.add(50);

// Close the controller
controller.close();
}

In Dart, streams can also be paused and resumed to control the flow of data to listeners. This feature is especially useful when you need to manage how data is processed or handle scenarios where the rate of incoming data might need to be adjusted.

1. Pausing a Subscription

  • Purpose: Temporarily halts delivery of events to a listener, allowing you to manage or buffer data without processing it immediately.
  • How It Works: When a subscription is paused, the stream stops sending data events to that listener. Events added to the stream during this pause are buffered (stored), meaning they are held until the subscription is resumed. This allows you to temporarily stop processing without losing any data.

2. Resuming a Subscription

  • Purpose: Resumes the delivery of data events to a listener that was previously paused.
  • How It Works: When a subscription is resumed, it starts receiving any buffered (stored) events that were added while it was paused. After processing these buffered events, the subscription continues to handle new events from the stream as they are emitted.

Example] Below example demonstrates how to create a stream, add data to it, pause the subscription, resume it, and handle events accordingly.


import 'dart:async';

void main() async {
final controller = StreamController<int>.broadcast();

final subscription = controller.stream.listen(
(data) {
print('Received data: $data');
},
onError: (error) {
print('Received error: $error');
},
onDone: () {
print('Stream closed.');
},
);

// Add initial data events
controller.add(1);
controller.add(2);

// Pause the subscription after a delay
await Future.delayed(Duration(seconds: 1));
subscription.pause();
print('Subscription paused.');

// Add more data events while paused
controller.add(3);
controller.add(4);

// Resume the subscription after another delay
await Future.delayed(Duration(seconds: 1));
subscription.resume();
print('Subscription resumed.');

// Add more data events after resuming
controller.add(5);
controller.add(6);

// Close the controller
await Future.delayed(Duration(seconds: 1));
controller.close();
}

Example] Below we create two different listeners (subscriptions) to a broadcast stream. One listener will be paused and then resumed while the other continues processing the stream’s events independently.


import 'dart:async';

void main() async {
// Create a StreamController with broadcast capability
final controller = StreamController<int>.broadcast();

// First listener
final subscription1 = controller.stream.listen(
(data) {
print('Listener 1 received: $data');
},
onError: (error) {
print('Listener 1 received error: $error');
},
onDone: () {
print('Listener 1: Stream closed.');
},
);

// Second listener
final subscription2 = controller.stream.listen(
(data) {
print('Listener 2 received: $data');
},
onError: (error) {
print('Listener 2 received error: $error');
},
onDone: () {
print('Listener 2: Stream closed.');
},
);

// Add initial data events
controller.add(1);
controller.add(2);

// Pause the first subscription
await Future.delayed(Duration(seconds: 1));
subscription1.pause();
print('Listener 1 paused.');

// Add more data events while the first subscription is paused
controller.add(3);
controller.add(4);

// Resume the first subscription after a delay
await Future.delayed(Duration(seconds: 2));
subscription1.resume();
print('Listener 1 resumed.');

// Add more data events after resuming
controller.add(5);
controller.add(6);

// Close the controller
await Future.delayed(Duration(seconds: 1));
controller.close();
}

NOTE : Each subscription has its own stream of events, so pausing or resuming one does not affect others. A paused listener stops receiving data temporarily, but other listeners continue to process events.


Stream.fromIterable()

The Stream.fromIterable constructor allows you to create a stream from an iterable collection (such as a list, set, or any iterable). It emits each element of the iterable sequentially as separate events in the stream.

When you pass an iterable to Stream.fromIterable, it iterates through the collection and adds each element to the stream. Each item is emitted one by one to any listeners.

NOTE : The Stream.fromIterable is a single-subscription stream. This means that it supports only one listener at a time.


void main() {

// Create a stream from an iterable
Stream<int> numberStream = Stream.fromIterable([1, 2, 3, 4, 5]);

// Listen to the stream
numberStream.listen(
(number) {
print('Received: $number');
},
onDone: () {
print('Stream completed');
},
onError: (error) {
print('Error: $error');
},
);
}

NOTE: With Stream.fromIterable, you cannot add more data to stream after its initial creation. This method creates a stream that only emits the items from the provided iterable, and once those items have been emitted, the stream completes.

If you need a stream where you can add new data over time, you should use a StreamController. This allows you to dynamically add data to the stream at any point during its lifecycle.


Stream.periodic()

The Stream.periodic constructor creates a stream that emits events at specified intervals. This is useful for generating a stream of events or values at regular time intervals, like a ticker or a periodic task.

When you use Stream.periodic, you specify 2 main things:

  • Interval (Duration): The time between each emission of an event.
  • Callback Function (Optional): A function that generates the value to be emitted and executed at intervals. This function receives an integer representing current iteration of emit (starting from 0) and returns the value to be emitted.

import 'dart:async';

void main() {
// Create a periodic stream that emits Fibonacci numbers every second
Stream<int> fibonacciStream = Stream.periodic(
Duration(seconds: 1), // Interval: 1 second
(count) => fibonacci(count) // Callback function to get Fibonacci number
);

// Listen to the stream
var subscription = fibonacciStream.listen(
(value) {
print('Received Fibonacci number: $value');
},
onError: (error) {
print('Error: $error');
},
);

// Stop receiving events after 15 seconds
Future.delayed(Duration(seconds: 15), () {
subscription.cancel();
print('Subscription cancelled');
});
}

// Function to compute Fibonacci number for a given index
int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}

The Stream.periodic creates an infinite stream that does not complete on its own. The stream continues to emit events at regular intervals indefinitely until you explicitly cancel the subscription.

To stop receiving events and effectively "end" the stream, you need to cancel the subscription. This is done using the cancel method on the subscription object obtained from the listen method.

---------------------------------------------------------------------------------------------------------------

Classes

A class in Dart is defined using the "class" keyword. Inside a class, you can declare fields (variables) and methods (functions) that define the state and behavior of the objects created from the class. Once a class is defined, you can create instances (objects) of that class using the "new" keyword or directly with the constructor.


class Animal {

// Fields (properties)
String name;
String species;

// Constructor to initialize the object
Animal(this.name, this.species);

// Method to display animal information
void displayInfo() {
print('This is a $species named $name.');
}

}

void main() {
// Creating an object of the Animal class
Animal myPet = Animal('Buddy', 'Dog');

// Calling the method on the object
myPet.displayInfo();
}


Constructors

In Dart, a constructor is a special method in a class that is used to create and initialize objects. Constructors allow you to assign values to the fields (properties) of the class when an object is created. They have the same name as the class and are automatically called when an object is instantiated.

Dart implements many types of constructors. Except for default constructors, these functions use the same name as their class :

  • Default constructors: Used to create a new instance when a constructor hasn't been specified. It doesn't take arguments and isn't named.
  • Named constructors: Clarifies the purpose of a constructor or allows the creation of multiple constructors for the same class.
  • Factory constructors: Either creates a new instance of a subtype or returns an existing instance from cache.


Default Constructor

In Dart, a default constructor is automatically provided by the language when a constructor is not explicitly defined in a class. It is a constructor that takes no arguments and is not named. The default constructor simply creates a new instance of the class without performing any specific initialization tasks.


class Person {
String name = 'John Doe';
int age = 30;
}

void main() {

// Dart automatically provides a default constructor
Person p = Person();
// Accessing fields initialized with default values
print('Name: ${p.name}, Age: ${p.age}');
}


Generative Constructor

A generative constructor in Dart is the most common type of constructor used to create and initialize new instances of a class. It allows you to set the initial state of an object by assigning values to its instance variables when the object is created.

The primary role of a generative constructor is to initialize the instance variables (fields) of an object. You can pass arguments to a generative constructor to initialize the instance variables. A generative constructor must have the same name as the class.


class Person {
String name = ''; // Initialized field
int age = 0; // Initialized field

// Generative Constructor
Person(String name, int age) {
this.name = name;
this.age = age;
}

void displayInfo() {
print('Name: $name, Age: $age');
}
}

void main() {
Person p = Person('Alice', 25);
p.displayInfo(); // Output: Name: Alice, Age: 25
}

The shorthand version of a generative constructor simplifies the process of initializing instance variables. Instead of manually assigning parameters to instance variables within the constructor body, Dart allows you to initialize them directly within the constructor's parameter list.


class Person {
String name; // Field for the person's name
int age; // Field for the person's age

// Shorthand Generative Constructor
Person(this.name, this.age);

// Method to display person details
void displayInfo() {
print('Name: $name, Age: $age');
}
}

void main() {
// Creating an object using the shorthand generative constructor
Person p = Person('Alice', 25);

// Accessing the method to display the object's data
p.displayInfo(); // Output: Name: Alice, Age: 25
}


Named Constructor

In Dart, named constructors provide a way to create multiple constructors for a single class, each with a different name. This allows for more flexible object creation and better readability, as you can use different constructor names to clearly indicate the purpose or type of initialization.


class Rectangle {
double width;
double height;

// Named Constructor for a square
Rectangle.square(double size)
: width = size,
height = size;

// Named Constructor for a rectangle with specific dimensions
Rectangle.withDimensions(double width, double height)
: width = width,
height = height;

// Named Constructor for a rectangle with a default height
Rectangle.withDefaultHeight(double width)
: width = width,
height = 10; // Default height value

// Method to display rectangle details
void displayInfo() {
print('Width: $width, Height: $height');
}
}

void main() {

// Using the named constructor for a square
Rectangle square = Rectangle.square(5.0);
square.displayInfo(); // Output: Width: 5.0, Height: 5.0

// Using the named constructor for specific dimensions
Rectangle rectangle = Rectangle.withDimensions(4.0, 6.0);
rectangle.displayInfo(); // Output: Width: 4.0, Height: 6.0

// Using the named constructor with a default height
Rectangle defaultHeightRectangle = Rectangle.withDefaultHeight(7.0);
defaultHeightRectangle.displayInfo(); // Output: Width: 7.0, Height: 10.0

}



class Rectangle {
double width;
double height;

// Default Constructor
Rectangle(this.width, this.height);

// Named Constructor for a square
Rectangle.square(double size) : this(size, size);

// Named Constructor for a rectangle with default height
Rectangle.withDefaultHeight(double width, [double height = 10.0])
: this(width, height);
}

void main() {
Rectangle rect1 = Rectangle(4.0, 6.0);
Rectangle square = Rectangle.square(5.0);
Rectangle defaultHeightRect = Rectangle.withDefaultHeight(7.0);

// Display rectangle information
print('Rectangle 1: Width=${rect1.width}, Height=${rect1.height}');
print('Square: Width=${square.width}, Height=${square.height}');
print('Default Height Rectangle: Width=${defaultHeightRect.width},
                                                Height=${defaultHeightRect.height}');
}

NOTE Dart does not support constructor overriding like other languages because constructors are not inherited in Dart. However, you can achieve similar functionality using named constructors and factory constructors.


Redirecting Constructor

Redirecting constructors in Dart provide a way to delegate the initialization logic of one constructor to another constructor within the same class. This feature is useful for simplifying complex object creation scenarios and ensuring consistent initialization logic across multiple constructors.

A redirecting constructor in Dart is primarily used to call another constructor within the same class. It does not have its own initialization logic but simply forwards the parameters to another constructor. 

NOTE : The syntax for redirecting constructors uses the colon syntax : this(...), allowing you to forward parameters from one constructor to another. 


class Rectangle {
final double width;
final double height;

// Primary Constructor
Rectangle(this.width, this.height);

// Named Constructor for a Square
Rectangle.square(double size) : this(size, size);

// Named Constructor with Default Height
Rectangle.withDefaultHeight(double width, [double height = 10.0])
: this(width, height);

// Named Constructor redirecting to another Named Constructor
Rectangle.squareWithDefaultHeight(double size)
: this.withDefaultHeight(size);
// Method to display rectangle details
void display() {
print('Width: $width, Height: $height');
}
}

void main() {

// Creating instances using different constructors
final rect1 = Rectangle(4.0, 6.0); // Uses the primary constructor
final square = Rectangle.square(5.0); // Uses the named constructor for a square
final defaultHeightRect = Rectangle.withDefaultHeight(7.0); // Uses the named constructor with default height
final squareWithDefaultHeight = Rectangle.squareWithDefaultHeight(8.0); // Redirects to the named constructor with default height

// Displaying the rectangles
rect1.display(); // Output: Width: 4.0, Height: 6.0
square.display(); // Output: Width: 5.0, Height: 5.0
defaultHeightRect.display(); // Output: Width: 7.0, Height: 10.0
squareWithDefaultHeight.display(); // Output: Width: 8.0, Height: 10.0
}

Within the context of a constructor, this is used in the : this(...) syntax to call another constructor within the same class. Outside of constructors, this refers to the current instance of the class. It is used to access instance variables and methods.

---------------------------------------------------------------------------------------------------------------

Inheritance

Inheritance in Dart is a fundamental object-oriented programming concept that allows you to create a new class based on an existing class. The new class, called the subclass or derived class, inherits properties and methods from the existing class, known as the superclass or base class.

The "extends" keyword is used to establish an inheritance relationship. Subclasses can override methods of the superclass and call superclass methods using the "super" keyword.


class Person {
String name;
int age;

Person(this.name, this.age);

void printSomething(){
print('Hello World !');
}

void displayInfo() {
print('Name: $name, Age: $age');
}
}

// Derived class
class Employee extends Person {
String jobTitle;

Employee(String name, int age, this.jobTitle) : super(name, age);

void displayInfo() {
print('Name: $name, Age: $age, Job Title: $jobTitle');
}
}

void main() {
var employee = Employee('Alice', 30, 'Software Engineer');
employee.displayInfo();
employee.printSomething();
// Output:
// Name: Alice, Age: 30, Job Title: Software Engineer
// Hello World !
}

The "super" keyword is used to access members of the superclass from within a subclass. This includes calling superclass constructors, methods, and accessing superclass properties.


// Base class
class Animal {
// Method in the base class
void makeSound() {
print('Some generic animal sound');
}
}

// Derived class
class Dog extends Animal {
void bark() {
// Calling the method from the base class
super.makeSound();
print('Woof! Woof!');
}
}

void main() {
// Creating an instance of Dog
var myDog = Dog();

myDog.bark();
// Output:
// Some generic animal sound
// Woof! Woof!
}

NOTE : In Dart, a class can extend only one other class. This is known as single inheritance.

We can use the "final" keyword to declare a class that cannot be extended. This means that no other class can inherit from a final class. Declaring a class as final ensures that it cannot be subclassed. This can be useful for classes that are intended to be used as-is and should not be extended.

// Declare a class as final
final class ImmutableClass {
String name;

ImmutableClass(this.name);

void display() {
print('Name: $name');
}
}

// Attempting to extend the final class will result in a compile-time error
class ExtendedClass extends ImmutableClass {
ExtendedClass(String name) : super(name); // This will cause an error
}

void main() {
ImmutableClass obj = ImmutableClass('Example');
obj.display(); // Output: Name: Example
}


Overriding Members

Overriding in Dart allows a subclass class to provide a specific implementation of a method, getter, setter, or operator that is already defined in its parent class. The overridden member in the subclass class has the same name and parameters as the member in the parent class. We can override the following thinsg in Dart :

  • Method Overriding: You can provide a new implementation of a method that is defined in the base class. The derived class method must match the method signature of the base class method.

  • Getter and Setter Overriding: You can override getters and setters to provide custom logic for property access and modification.

  • Operator Overriding: Allows customization of operators such as +, -, *, etc., for instances of a class.

NOTE : Use the @override annotation to indicate that a method, getter, setter, or operator is overriding a base class member.


// Base class
class Animal {
void makeSound() {
print('Some generic animal sound');
}
}

// Derived class
class Dog extends Animal {
@override
void makeSound() {
print('Dog barks');
}
}

void main() {
// Create an instance of the derived class
Dog dog = Dog();
// Call the overridden method
dog.makeSound(); // Output: Dog barks
}


Rules for Overriding :

  • Method Signature: The overriding method must have the same name and parameter types as the method in the base class.
  • Return Type: The return type of the overriding method must be the same or a subtype of the return type in the base class.
  • Annotations: Use the @override annotation to indicate that a method, getter, setter, or operator is overriding a base class member.

In Dart, we can override operators to define custom behavior for the standard operators when applied to instances of your classes. This allows you to tailor the behavior of operators like +, -, *, ==, and more to fit the specific needs of your class.

By overriding operators we define the specific behavior of operations when they are applied to instances of your class. This means that when you use an operator like +, -, ==, or others on objects of your class, it will execute the logic you’ve defined.

To override an operator in Dart, you use the operator keyword followed by the operator symbol. Below is an general syntax for overriding any operator in Dart classes :


class ClassName {
// Operator Method
ClassName operator +(ClassName other) {
// Custom implementation
}
}

Example] In below examples we override the + and == operators and define what happens when used among 2 different instances of a class.

class Vector {
final double x;
final double y;

Vector(this.x, this.y);

// Overriding the + operator
Vector operator +(Vector other) {
return Vector(x + other.x, y + other.y);
}

@override
String toString() => 'Vector(x: $x, y: $y)';
}

void main() {
Vector v1 = Vector(2, 3);
Vector v2 = Vector(4, 5);
Vector result = v1 + v2;
print(result); // Output: Vector(x: 6.0, y: 8.0)
}


class Point {
final double x;
final double y;

Point(this.x, this.y);

// Overriding the == operator
@override
bool operator ==(Object other) {
if (other is Point) {
return x == other.x && y == other.y;
}
return false;
}

@override
int get hashCode => x.hashCode ^ y.hashCode;
}

void main() {
Point p1 = Point(1, 2);
Point p2 = Point(1, 2);
print(p1 == p2); // Output: true
}

It's a good practice to override toString(), hashCode, and == in custom classes, especially when dealing with equality checks or printing object information. 

---------------------------------------------------------------------------------------------------------------

Abstraction

Abstraction refers to the concept of hiding implementation details and exposing only the necessary functionality. An abstract class is a class that cannot be instantiated directly. Instead, it serves as a blueprint for other classes to follow. 

Abstract classes can have both abstract methods (methods without body) methods (methods with body). Subclasses that extend an abstract class must provide implementations for the abstract methods.


abstract class ClassName {

// Abstract method
void methodName();

// Concrete method
void anotherMethod() {
// Implementation
}
}

NOTE : The "abstract" keyword is used to define abstract classes and abstract methods. Any method declared inside an abstract class without a body or implementation is considered an abstract method.


abstract class Animal {

// Abstract method (no body/implementation)
void sound();

// Concrete method (with body/implementation)
void sleep() {
print("Sleeping...");
}
}

class Dog extends Animal {
// Providing implementation for the abstract method
@override
void sound() {
print("Bark!");
}
}

void main() {
Dog dog = Dog();
dog.sound(); // Output: Bark!
dog.sleep(); // Output: Sleeping...
}


---------------------------------------------------------------------------------------------------------------

Interfaces

An interface is a class that defines a contract of methods and properties. Classes that implement this interface must provide concrete implementations for all the methods and properties declared in the interface.

In Dart, we use the "interface" keyword to define an interface class. To implement an interface, use the "implements" keyword followed by the interface class name. This indicates that the implementing class must provide implementations for all abstract methods and properties defined in the interface.




interface class Engine{

void mileage(int meters) {
}

void Revv(){

}

}

interface class Vehicle{

void moveForward(int meters) {
}

void goFast(){

}

}


class Honda implements Vehicle, Engine {

@override
void goFast() {
print('Going Fast !');
}

@override
void moveForward(int meters) {
print('Going Forward !');
}
@override
void Revv() {
print('REVVVEEV !!!');
}
@override
void mileage(int meters) {
print('Mileage is ${meters} kmpl');
}
}


void main(){

Honda civic = new Honda();
civic.moveForward(100);
civic.Revv();
civic.mileage(25);

}

NOTE : The "interface" keyword is supported in Dart only after Dart version 3.0.

A class can implement multiple interfaces by listing them separated by commas after the implements keyword. An Interface is similar to an Abstract class , but it can only consists of abstract methods.

---------------------------------------------------------------------------------------------------------------

Mixins

Mixins in Dart provide a way to reuse code across multiple classes without using traditional inheritance. A mixin is a special type of class in Dart that is intended to be used for adding functionality to other classes. 

Unlike inheritance, which creates an "is-a" relationship, mixins create a "has-a" or "can-do" relationship, allowing classes to share behavior. Mixins are a way of defining code that can be reused in multiple class hierarchies.

Some rules to keep in mind while using Mixins :

  • No Constructors : Mixins cannot have constructors as they are used solely for adding functionality.
  • No Direct Instantiation : You cannot create instances of a mixin directly.
  • Cannot Be Abstract : Mixins cannot be declared as abstract; they provide concrete implementations.
  • Must Be Applied to a Class : Mixins must be used with a class; they cannot be used standalone.
  • No Super Calls : Mixins cannot use super to call methods from other mixins or classes.

NOTE : A mixin is defined using the "mixin" keyword. To use a mixin, apply it to a class using the "with" keyword. The mixin will then provide its functionality to the class.


// Define the first mixin named 'Logger'
mixin Logger {
void log(String message) {
print('Log: $message');
}
}

// Define the second mixin named 'Notifier'
mixin Notifier {
void notify(String message) {
print('Notification: $message');
}
}

// Define a class that uses both 'Logger' and 'Notifier' mixins
class MyApp with Logger, Notifier {
String appName;

MyApp(this.appName);

void runApp() {
print('Running $appName');
log('App has started');
notify('App is now running');
}
}

void main() {
// Create an instance of 'MyApp'
MyApp myApp = MyApp('SuperApp');
myApp.runApp();
// Output:
// Running SuperApp
// Log: App has started
// Notification: App is now running
}

NOTE : The Mixins are supported in Dart only after Dart version 3.0.

If multiple mixins define fields with the same name, the field from the mixin listed last in the sequence will be used. Dart does not allow multiple mixins to define fields with the same name unless there is a specific need to override.


// Define the first mixin with a method
mixin MixinA {
void doSomething() {
print('MixinA');
}
}

// Define the second mixin with the same method name
mixin MixinB {
void doSomething() {
print('MixinB');
}
}

// Define a class that uses both mixins
class MyClass with MixinA, MixinB {
void test() {
doSomething(); // Calls the method from the last mixin in the order
}
}

void main() {
var myObject = MyClass();
myObject.test(); // Output: MixinB
}

The order in which mixins are applied determines method and field resolution. Be careful with the sequence if mixins provide methods or fields with the same names.

---------------------------------------------------------------------------------------------------------------

Generics

Generics enable you to define a class or function with placeholders for types. Instead of specifying a concrete type, you use a generic type parameter. This parameter can then be replaced with a specific type when you create an instance of the class or call the function.

  • Type Safety: Generics ensure that you are working with the correct types, reducing runtime errors and type mismatches.
  • Code Reusability: Write generic code that can handle different types without duplicating code for each type.
  • Flexibility: Create data structures and algorithms that can work with a variety of types.

Examples] Below we define generic class and methods which can work with different types.


class Box<T> {
T? item;

void putItem(T newItem) {
item = newItem;
}

T? getItem() {
return item;
}
}

void main() {
var intBox = Box<int>();
intBox.putItem(123);
print(intBox.getItem()); // Output: 123

var stringBox = Box<String>();
stringBox.putItem("Hello");
print(stringBox.getItem()); // Output: Hello
}



// Generic method to print all elements in a list
void printList<T>(List<T> items) {
for (var item in items) {
print(item);
}
}

void main() {
// Using the generic method with a list of integers
List<int> numbers = [1, 2, 3, 4, 5];
print('Integer List:');
printList(numbers);

// Using the generic method with a list of strings
List<String> words = ['apple', 'banana', 'cherry'];
print('\nString List:');
printList(words);

// Using the generic method with a list of doubles
List<double> decimals = [1.1, 2.2, 3.3];
print('\nDouble List:');
printList(decimals);
}


Constraints with Generics

In Dart, you can use constraints with generic types to restrict the types that can be used as type arguments for a generic class or method. Constraints are useful when you want to ensure that the generic type T adheres to certain requirements or inherits from a specific class.

To apply constraints, you use the "extends" keyword to specify that the generic type T must extend a particular class or implement a specific interface.


// Define the base class
class Person {
String name;
Person(this.name);
}

// Define the constrained generic class
class NamePrinter<T extends Person> {
T person;

NamePrinter(this.person);

void printName() {
print(person.name);
}
}

// Define subtypes of Person
class Employee extends Person {
Employee(String name) : super(name);
}

class Student extends Person {
Student(String name) : super(name);
}

void main() {
// Create instances of Person subtypes
var employee = Employee('Alice');
var student = Student('Bob');

// Create NamePrinter instances for Employee and Student
var employeePrinter = NamePrinter<Employee>(employee);
var studentPrinter = NamePrinter<Student>(student);

// Print names
employeePrinter.printName(); // Output: Alice
studentPrinter.printName(); // Output: Bob
}


---------------------------------------------------------------------------------------------------------------

Access Modifiers

In Dart, access modifiers control the visibility and accessibility of classes, methods, properties, and variables within a program. Unlike some other languages like Java, C#, or C++, Dart does not use explicit keywords like private, protected, or public

Instead, Dart uses a simpler convention where a member's visibility is determined by whether its name starts with an underscore ( ). Dart only has 2 levels of visibility:

  • Public (default): When the name of a class, method, or variable does not start with an underscore (_), it is public.
  • Private: When the name of a class, method, or variable starts with an underscore (_), it is private to its library.


void main() {

// Creating an instance of the Car class
Car myCar = Car('Tesla', 'Model S', 2023);

// Accessing public attributes
print('Car make: ${myCar.make}');
print('Car model: ${myCar.model}');
// Accessing public methods
myCar.displayInfo();

// Trying to access a private attribute
print(myCar._year);

// Trying to access a private method
myCar._startEngine();

// Using a public method to access a private attribute
print('Car year: ${myCar.getYear()}');
}

class Car {

// Public attributes (no underscore)
String make;
String model;

// Private attribute (with underscore)
int _year;

// Constructor
Car(this.make, this.model, this._year);

// Public method to display information
void displayInfo() {
print('Car: $make $model');
}

// Private method (with underscore)
void _startEngine() {
print('Engine started for $make $model.');
}

// Public method to access the private attribute
int getYear() {
// Can call the private method from within the class
_startEngine();
return _year;
}

}

The private members in Dart are private to the library, not to the class. A library in Dart refers to a single file or a collection of related files that are grouped together.

  • Public members: Accessible from anywhere in the code, including other libraries.
  • Private members: Accessible only within the same library. A member prefixed with an underscore (_) cannot be accessed outside the file or library it is defined in.
  • The underscore _ is the only way to define private members in Dart. There are no explicit keywords like private or protected.

NOTEUnderscore fields, classes and methods will only be available in the .dart file where they are defined.

---------------------------------------------------------------------------------------------------------------







Comments

Popular posts from this blog

React Js + React-Redux (part-2)

React Js + CSS Styling + React Router (part-1)

ViteJS (Module Bundlers, Build Tools)