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<Uint8List>
:
// 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<Uint8List> downloadUrl() => Completer<Uint8List>().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); }
Where to Go From Here
As part of the Free Dart Course on YouTube, I’ve recorded a video that explains it all with mixins and you can find that video here. I highly suggest that you watch this video as a complimentary material to this article. After that, it’s really a matter of practicing to use mixins in your code to get better.
The tip on using Union mixins in Dart is useful for me. Thanks for sharing.
I’m really glad to hear that, Hashem, and thanks for your feedback
Amazing explanation. Thank you Vandad
I’m glad you liked it. And thank you for your feedback
Vandad, thanks so much for such an awesome explanation. It’s always great to read your articles
I’m very glad to hear that. Thank you
You have amazing explanation of mixin with esay to understand. You have great knowledge deep dive.
Thank you brother