When developing an app with multiple screens, we tend to reuse the same piece of code over many classes: showing error messages, using the same page layout and wiring up some dependencies like for example a Bloc. All this could be solved if were using an abstract
base class, however, what if we have a set of features/classes that we want to use on a particular screen but not on others? Since a class can't be a child of more than one class, should we create different base classes, as much as the number of combinations that we have? That's why we have mixin
s.
Mixins and Base Classes: An introduction
Mixins let us add a set of “features” to a class without using parent-child hierarchy, allowing us to have in the same class one parent and multiple mixin
components. As such, since it’s not a parent of our class, mixin
s don't allow any declaration of constructors. You can read more about them in this article by Romain Rastel with the caveat that Dart 2 now has the mixin
keyword, as seen in the documentation.
But how do mixins
work? Let's take as an example an abstract class
Person
abstract class Person {
void think() {
print("Hmm, I wonder what I can do today");
}
}
We can use this class as a parent using the extend
keyword, such as:
class Mike extends Person {}
With this, we can initialize the class and call the parent’s method think()
.
void main() {
var mike = Mike();
mike.think(); // prints: "Hmm, I wonder what I can do today"
}
But what if we want to add new features to Mike
? What if Mike is a coder and needs a code
function that could be used in other Person
, but is not used by all Person
s? mixin
s save that problem.
First, we need to create a mixin
class and expose the new methods we want to use.
mixin Coder {
void code() {
print("Coding intensifies");
}
}
With the keyword with
, we can add this "feature" to our Mike
class:
class Mike extends Person with Coder {}
And, as with the parent, we can call all the functions that we created in Coder
.
void main() {
var mike = Mike();
mike.code(); // prints: "Coding intensifies"
}
Now every class that uses the mixin
Coder
can effectively code. However, this poses a problem: this means that if we have a parent class Animal
that has a child Squirrel
, we can also have a Squirrel
that can code()
! To prevent this, we can "lock" the usage of the mixin
to a class and all classes that inherited from it with the keyword on
:
mixin Coder on Person{
void code() {
print("Coding intensifies");
}
}
This also gives us a powerful tool: we can now override methods that were set in the Person
class to add or change functionality.
mixin Coder on Person{
//...
@override
void think() {
super.think();
print("I'm going to code today!");
}
}
Calling the super.think()
ensures that we are still calling the code that was defined in Person
. The above code gives us the following output for the method think
in. Mike
:
Hmm, I wonder what I can do today
I'm going to code today!
By grasping the concepts of both base class
es and mixin
s we can now apply them to our Flutter apps.
Mixins and Base Classes: A practical Flutter Example
How can we apply this in our Flutter apps?
Take as an example the following two screens:
Our app has several screens with the layout shown above. Instead of copying and pasting the appbar and the background for each screen, we can solve our problem using mixin
s.
In both cases we have a screen title defined, we are going to create a base class
that has a method to provide our screen name called BasePage
. We also are going to apply the mixin
s only in StatefulWidgets
since our classes will maintain and change their state. With this, we create two classes to be used in our pages: a BasePage
and a BaseState<BasePage>
that extend StatefulWidget
and State<StatefulWidget>
respectively.
abstract class BasePage extends StatefulWidget {
BasePage({Key key}) : super(key: key);
}
abstract class BaseState<Page extends BasePage> extends State<Page> {
String screenName();
}
Focusing on the second screen, we can now create its custom mixin
BasicPageMixin
, where we define the background and appbar of our page.
mixin BasicPage<Page extends BasePage> on BaseState<Page> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(screenName()),
),
body: Container(
child: body(),
color: Colors.amber,
));
}
Widget body();
}
Since the method body()
doesn't have a body, each class that uses this mixin
must implement it, ensuring that we don't forget to add a body to our page.
In the screenshot above, we see a FloatingActionButton
, but we may not need it for every screen, so how can we define it? By declaring a new method, fab()
that by default returns a Container
. If a class needs to add a FloatingActionButton
, they can override this method.
mixin BasicPage<Page extends BasePage> on BaseState<Page> {
@override
Widget build(BuildContext context) {
return Scaffold(
//...
floatingActionButton: fab(),
}
//...
Widget fab() => Container();
}
With our mixin
created, we can apply it to a new With this set, we can now apply it to a new page.
class MyHomePage extends BasePage {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends BaseState<MyHomePage> with BasicPage{
@override
String screenName() => "Home";
@override
Widget body() {
return Center(child: Text("This is a basic usage of a mixin"));
}
}
And with this, we now only have to declare a body()
and a possible fab()
widget to be used in each screen, saving us a few dozens of lines of code.
Combining mixins
As a new feature, some of our screens will make API calls and if an error occurs, we need to display an error message in the form of a Snackbar. Additionally, we decide to use the BLoC architecture in which we need to inject a new bloc when each page is created. These two problems will need the following steps:
- Change our
BasePage
by changing its constructor with the newbloc
parameter. - Change
BaseState
by adding a newGlobalKey<ScaffoldState>
- Create a new
mixin
that let us display errors messages sent by thebloc
in the page using a Snackbar
In our BaseBloc
we are just exposing a Sink
and a Stream
in order to relay error messages.
abstract class BaseBloc {
/// relays error information
final _errorSubject = PublishSubject<String>();
Sink<String> get errorSink => _errorSubject.sink;
Stream<String> get errorStream => _errorSubject.stream;
}
Since we don’t want any other interactions with the bloc
, our HomeBloc
will just extend this class
class HomeBloc extends BaseBloc {}
We proceed by changing the constructor of our BasePage
to include the bloc
object. This will force us to also change all the classes that extend it to add the bloc
to their constructors too. The bloc
parameter is used as a Generic type so that each class that extends it can declare the correct type of bloc it is using. This makes sure that when we are calling it in the BaseState
, we are going to get the correct type of bloc
allowing us to access its methods.
abstract class BasePage<Bloc extends BaseBloc> extends StatefulWidget {
Bloc bloc;
BasePage({Key key, this.bloc}) : super(key: key);
}
As for the BaseState
, we are going to declare a scaffoldKey
to be used with the ScaffoldWidget
so that we can show the Snackbar.
abstract class BaseState<Page extends BasePage> extends State<Page> {
String screenName();
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
}
As seen before, one of the curious properties of mixing is that if they are “linked” to a class, they can override
its methods. This is useful since in our StatefulWidget
we can listen
do the bloc
's streams
in the initState
method. As such, to show the error messages, we can create a mixin
that overrides the initState
method and provides methods to show the error Snackbar with the correct message.
mixin ErrorHandlingMixin<Page extends BasePage> on BaseState<Page> {
@override
void initState() {
super.initState();
widget.bloc.errorStream
// this will preven multiple messages from showing up in a short interval of time
.transform(new ThrottleStreamTransformer((_) => TimerStream(true, const Duration(seconds: 2))))
.listen((error) => showErrorSnackbar(error, scaffoldKey.currentState));
}
void showErrorSnackbar(String event, ScaffoldState context) {
if (event != null) {
context.showSnackBar(new SnackBar(
content: new Text(event),
));
}
}
}
Finally, we can add it to our HomePage
class by adding it after the BasicPage
mixin.
class _MyHomePageState extends BaseState<MyHomePage> with BasicPage, ErrorHandlingMixin {
//...
@override
Widget fab() => FloatingActionButton(
child: Icon(Icons.error),
onPressed: () => widget.bloc.errorSink.add("A new error!"),
);
}
Conclusion
And there we go! ✌️ Now we can use both mixin
s and abstract class
es to reuse code throughout our application.
Maybe we don't need to do a base UI for our app, but we can use mixin
s like the ErrorHandlingMixin
to provide error feedback to the user, loading screens and showing a "App is Offline" screen.
However, creating both base classes and mixin
s is a process that needs some deliberation, else we might face a "Deadly Diamond of Death", in which when calling one method that is declared in both base classes and mixin
s, the compiler doesn't know which one to choose. You can read more about it in Shubham Soni 's article about mixin
s in Flutter.
Finally, and as suggested by Remi Rousselet, we should be aware of how the extensive use of mixin
s can be considered an anti patterns, which you can read more in this article by Dan Abramov.