Mixins are reusable pieces of code that can be brought into another class or mixin. In Swift they are called protocols; in Rust they are called traits and in JavaScript, well, they are called mixins too! In most object oriented programming languages such as Dart, a class can inherit from maximum of 1 other class meaning that Dart doesn’t support multiple inheritance, unlike languages such as C++. In this article, we will have a look at what mixins are in Dart and why they are useful and so powerful.
Anatomy of a Mixin in Dart
Every mixin in Dart starts with the mixin keyword, followed by the name of the mixin in PascalCase, optionally followed by the keyword on and the name of an existing mixin to which you want to add your new mixin! Here is an example:
mixin Foo {
// the original mixin
// we haven't written any code
// in this mixin yet
}
mixin Bar on Foo {
// a new mixin that is programmed
// to automatically sit on top of the
// Foo mixin and have access to Foo's
// members
}
The Bar mixin is more advanced and we will learn more about it later but for now it’s important to understand that the Foo mixin is supposed to be a reusable piece of logic that we can blend into other classes and the Bar mixin attaches itself to the Foo mixin so that Bar can have access to all members of Foo.
Mixins as Specifications
Mixins in Dart can be used to specify what a class has to do. A blueprint of some sort if you will. In Swift, this is done through protocols:
import Foundation
protocol HasFirstName {
var firstName: String { get }
}
struct Person: HasFirstName {
let firstName: String
}
The HasFirstName protocol tells the Swift compiler that any class or structure (structures are available in Rust and Swift but not in Dart) that uses this protocol (mixin), has to implement the firstName property. The same is true in Dart, you can implement the same code in Dart as shown here:
// this mixin asks for any types that implement
// it to have a property called "firstName"
mixin HasFirstName {
String get firstName;
}
// the person class then implements the HasFirstName mixin
// by overriding the "firstName" property
class Person with HasFirstName {
@override
final String firstName;
const Person(this.firstName);
}
And the same can be done in Rust using traits as mentioned. I want you to see how similar these concepts are across languages in general:
// the same first name trait but in Rust
// the trait needs to expect a function
// rather than a variable
trait HasFirstName {
fn first_name(&self) -> &str;
}
// the Person struct, exactly like the
// Person class in Dart
struct Person {
first_name: String,
}
// we then implement the HasFirstName trait
// on the Person struct with this syntax
impl HasFirstName for Person {
fn first_name(&self) -> &str {
&self.first_name
}
}
What’s important to note about these traits is that they don’t have any logic of their own; all they are doing is expecting the class that conforms to them to implement the required properties and methods. That’s when mixins are useful as specifications or blueprints.
Mixins with Logic
Apart from being blueprints for implementing classes, mixins can contain logic as well. Meaning that they don’t just have to sit there and specify what methods and variables implementing classes have, but they can have some logic and code of their own.
Let’s implement a mixin that expects a first and a last name from its implementing classes and gives them an implementation of a fullName for free:
import 'dart:developer' as devtools show log;
extension Log on Object {
void log() => devtools.log(toString());
}
mixin HasName {
// expect a first and a last name from
// any implementing classes
String get firstName;
String get lastName;
// using the first and the last name properties
// implement a "fullName" property which our
// implementing classes will get for free
String get fullName => '$firstName $lastName';
}
// we then define a Person class that uses our mixin
@immutable
class Person with HasName {
// firstName and lastName properties need to be
// overridden since they have no default
// implementation in the mixin
@override
final String firstName;
@override
final String lastName;
// but we don't have to implemnent the fullName
// property since it is already defined in the mixin
const Person(this.firstName, this.lastName);
}
void testIt() {
// we then create an instance of the Person class
// and use the fullName property for free
const person = Person('Vandad', 'Nahavandipoor');
person.fullName.log();
}
What’s interesting about the HasName mixin is that it expects any implementing classes to override the firstName and lastName properties but since it knows the class has to have those two properties, it uses them inside the fullName property to deliver “free” logic to the class.
In the previous example we saw how a mixin gave us access to a new property called fullName but mixins are not limited to providing us with variables. They can also have functions. For instance, we can write a mixin that expects any implementing type to have a url property and given that property, it will give us a free implementation of a method that can download the contents of that URL as Future:
// add our imports
import 'dart:async' show Completer;
import 'dart:typed_data' show Uint8List;
mixin HasUrl {
// this mixin expects any implementing types
// to have a url
property
String get url;
// then using the URL property, it gives
// us the implementation of the "downloadUrl" method
Future downloadUrl() => Completer().future;
}
@immutable
// we then specify our concrete type that implements the mixin
class MyAPI with HasUrl {
// we have to specify the url
property
// as requested by the mixin
@override
String get url => 'https://foobar.com';
// then we can use the downloadUrl
method without
// having to implement that method ourselves
const MyAPI();
}
void testIt() async {
const api = MyAPI();
// and then put it to use
final data = await api.downloadUrl();
// do something with "data" here
}
Multiple Mixin Conformance
As mentioned during the introduction to this article, Dart doesn’t have support for multiple inheritance with classes but you can always use multiple mixins with the help of the with keyword. Let’s have a look at an example:
import 'dart:developer' as devtools show log;
extension Log on Object {
void log() => devtools.log(toString());
}
// a mixin that expects a name property
// to be implemented on the class
mixin HasName {
String get name;
}
// a mixin that expects the implementing type
// to first implement the HasName mixin
// and given that, it can then use the name property
// to log the name of the class as part of the
// accelerate method
mixin CanAccelerate on HasName {
void accelerate() {
'$name is accelerating'.log();
}
}
// same as the CanAccelerate mixin, but this time
// we're using the HasName mixin to get the name
// and then decelerating the object
mixin CanDecelerate on HasName {
void decelerate() {
'$name is decelerating'.log();
}
}
// A class that puts all the three mixins into use
// by implementing the HasName mixin
// and hence receiving the accelerate and decelerate
// functions for free
class Car with HasName, CanAccelerate, CanDecelerate {
@override
final String name;
const Car(this.name);
}
void testIt() {
const myCar = Car('Tesla Model X');
// we can then accelerate the car
myCar.accelerate();
// or decelerate it
myCar.decelerate();
}
The code mixin CanAccelerate on HasName means that the CanAccelerate mixin is available only to classes that choose to mix the HasName mixin. That’s why the Car class is mixing the HasName mixin and then the CanAccelerate and CanDecelerate mixins. Without mixing the HasName mixin, the Car class wouldn’t be able to mix the CanAccelerate and CanDecelerate mixins since they depend on the presence of the HasName mixin.
Testing for Mixin Conformance
If you ever receive an object in any function, and want to test whether that object conforms to a specific mixin, you can use the is syntax in Dart:
// tests whether a given object
// conforms to the HasName mixin
bool doesObjectHaveName(Object obj) => obj is HasName;
The opposite of the is keyword in Dart, a keyword that not many people know about, is the is! keyword with an exclamation mark at the end of the is keyword. That simply means “is not”. So if you ever have to check whether object is not conformant to a specific mixin, you can use that keyword. Here is an example:
// a function that takes in any object and tests
// whether that object conforms to the HasName mixin
// or not but this time tests the negative case
// first using the "is!" operator
void describe(Object obj) {
if (obj is! HasName) {
'This object has no name'.log();
} else {
'This object has a name: ${obj.name}'.log();
}
}
void testIt() {
// prints: This object has a name: Tesla Model S
describe(const Car('Tesla Model S'));
// prints: This object has no name
describe(10);
}
Mixins as Parameter Types
Mixins are great ways of helping you cherry-pick important parts of your code into their own isolated spaces. Let’s say that you have two mixins, one that specifies a name and another one that specifies the age of, say, a person:
// this mixin expects a name
mixin HasName {
String get name;
}
// and this one expects an age
mixin HasAge {
int get age;
}
Then you can have a Person class that uses both these mixins as shown here:
// a person class that uses both these mixins
// and overrides the required instance methods
// as per the mixins
class Person with HasName, HasAge {
@override
final String name;
@override
final int age;
const Person(this.name, this.age);
}
Now if you were asked to write a function that returns a boolean to determine if a given person is at least of 18 years of age or not, you could write it like this:
// a method that takes in a whole Person
// object to determine if he/she is older than
// a given age (18 in this case)
bool isAtLeast18YearsOld(Person person) => person.age >= 18;
However, inside this function, all you are doing is accessing only the age property of the given Person object so you might as well change the implementation so that instead of taking in a whole Person object, you take in an object that conforms to the HasAge mixin as shown here:
// a method that takes in any object
// that conforms to the HasAge mixin
bool isAtLeast18YearsOld(HasAge person) => person.age >= 18;
By changing your code according to the above, you can reuse this function on any class that uses the HasAge mixin, even if it’s a dog or a cat:
// a Dog class that also conforms to the
// HasAge mixin
class Dog with HasAge {
@override
final int age;
const Dog(this.age);
}
void testIt() {
// this code now works for not only a Person instance
isAtLeast18YearsOld(const Person('John Doe', 30)).log();
// but also for a Dog instance
isAtLeast18YearsOld(const Dog(13)).log();
}
This works nicely for one mixing as the type of the parameter but if you have two or more mixins and you want a union type of them inside the parmeter type, you’re out of luck because Dart doesn’t support union types as of yet. Other languages such as Rust support it though.
Mixin Unions
As mentioned in the previous section, Dart doesn’t support union type of mixins yet. So the following code won’t work:
// this mixin expects a name
mixin HasName {
String get name;
}
// and this one expects an age
mixin HasAge {
int get age;
}
// 🚨 this won't compile since in Dart you cannot
// create a union of two mixins with & or any other
// operator at the time of this writing
void describe(HasName & HasAge person) {
'${person.name} is ${person.age} years old.'.log();
}
Some other languages such as Swift have a much nicer way of dealing with unions and that’s through the & operator as shown here:
import Foundation
// a protocol that expects a name
protocol HasName {
var name: String { get }
}
// and another protocol that expects
// an age property
protocol HasAge {
var age: Int { get }
}
// you can then create a union of the two
// protocols using the & operator
func describe(person: HasName & HasAge) -> String {
String(
format: "%@ is %d years old",
person.name, // name from HasName
person.age // age from HasAge
)
}
In other languages such as Rust, this is a bit more complicated to do as you would have to create a separate trait (mixin) that mixes the other traits to create a whole new trait as shown here:
![deny(clippy::all)]
// a trait that expects a name function
// to be implemented on the type
trait HasName {
fn name(&self) -> &str;
}
// another one that expects an age function
// to be implemented on the type
trait HasAge {
fn age(&self) -> u32;
}
// a trait union is a union of multiple traits
// and can be imposed on a type
trait HasNameAndAge: HasName + HasAge {
// empty for now
}
// a function that uses the trait union
// named HasNameAndAge and uses the name
// and age functions
fn describe_obj(obj: &dyn HasNameAndAge) {
println!("I am {} years old and my name is {}", obj.age(), obj.name());
}
// a simple Person struct that already has
// the 2 required properties we need
struct Person {
name: String,
age: u32,
}
// implement the HasName trait on the Person struct
impl HasName for Person {
fn name(&self) -> &str {
&self.name
}
}
// and finally implement the HasAge trait
// on the Person struct
impl HasAge for Person {
fn age(&self) -> u32 {
self.age
}
}
// this would be an empty implementation
// since Person already conforms to the
// HasNameAndAge through implementation of
// HasName and HasAge
impl HasNameAndAge for Person {
// empty for now
}
fn main() {
let p = Person {
name: "John".to_string(),
age: 30,
};
// we can pass Person to describe_obj
// because it implements HasNameAndAge
describe_obj(&p);
}
Surprisingly, you can do a similar thing in Dart by combining two or more mixins into a new mixin and have that new mixin union type as your parameter types, as shown here:
import 'dart:developer' as devtools show log;
extension Log on Object {
void log() => devtools.log(toString());
}
// this mixin expects a name
mixin HasName {
String get name;
}
// and this one expects an age
mixin HasAge {
int get age;
}
// a mixin that expects a name and an age
// by creating a union of the two
mixin HasNameAndAge on HasName, HasAge {}
// create a class that conforms to the mixins
@immutable
class Person with HasName, HasAge, HasNameAndAge {
@override
final String name;
@override
final int age;
const Person(this.name, this.age);
}
// and you can use this new mixin
// as a data type into a function
void describe(HasNameAndAge p) {
'${p.name} is ${p.age} years old.'.log();
}
void testIt() {
const p = Person('Vandad', 32);
// you can then pass "p" to the function
// and it will work as expected
describe(p);
}
Top comments (0)