DEV Community

Elijah L.
Elijah L.

Posted on

When can Dart's mixins become helpful in Flutter

The Dart mixin can be a powerful tool in your projects. But without proper care, it can lead to unnecessary, un-scalable complexity.

So when should a mixin be used? According to Dart:

Mixins are a way of reusing a class’s code in multiple class hierarchies.

Navigation & Routes

Let's go through an example where we can move a projects navigation to a mixin. Here we have two simple pages: HomePage and DetailsPage:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: TextButton(
          child: Text('DETAILS PAGE'),
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => DetailsPage()),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

home page

class DetailsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details Page')),
      body: Center(
        child: TextButton(
          onPressed: Navigator.of(context).pop,
          child: Text('GO BACK'),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

details page

At this time we are pushing the DetailsPage as most probably would using the Navigator.push method. But what happens when have many different places the page is being pushed from? This leads to re-writing the same push call many times.

Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => DetailsPage()),
)
Enter fullscreen mode Exit fullscreen mode

But this can be moved into a mixin and can be added to our HomePage class!

mixin MyRoutesMixin on Widget {
  Future<T?> pushDetailsPage<T extends Object?>(BuildContext context) =>
      Navigator.push<T>(
        context,
        MaterialPageRoute(builder: (_) => DetailsPage()),
      );
}
Enter fullscreen mode Exit fullscreen mode
class HomePage extends StatelessWidget with MyRoutesMixin {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: TextButton(
          child: Text('DETAILS PAGE'),
          onPressed: () => pushDetailsPage(context),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's continue building off of this example.

Logging & Telemetry

Let's say you now have a logger that needs to track when a button is pressed. And your Logger class looks like this (pretend that it's actually sending data to an endpoint on the log() method):

class Logger {
  const Logger();
  void log(String label, DateTime dateTime) {
    print('PRESSED: $label at $dateTime');
    // TODO: complete the logger
  }
}
Enter fullscreen mode Exit fullscreen mode

But you shouldn't have to create a new Logger instance each time you need to use it in a widget. So let's do that in a mixin:

mixin LoggerMixin {
  static const _logger = Logger();
  void logButtonPressed(String label) => _logger.log(label, DateTime.now());
}
Enter fullscreen mode Exit fullscreen mode

Let's add it to our DetailsPage widget and test it with a new button.

class DetailsPage extends StatelessWidget with LoggerMixin {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details Page')),
      body: Center(
        child: Column(
          children: [
            TextButton(
              onPressed: Navigator.of(context).pop,
              child: Text('GO BACK'),
            ),
            TextButton(
              // Using the new mixin here
              onPressed: () => logButtonPressed('EXAMPLE'),
              child: Text('EXAMPLE'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

details page

Common Strings

Sometimes you will need something as simple as text and other data accessible to your widgets. A mixin be the perfect example for that. Let's move all of our "commonly" used Strings into a mixin.

mixin MyApplicationStrings {
  final example = 'EXAMPLE';
  final goBack = 'GO BACK';
  final detailsPage = 'Details Page';
  final homePage = 'Home Page';
}
Enter fullscreen mode Exit fullscreen mode
class HomePage extends StatelessWidget
    with MyRoutesMixin, MyApplicationStrings {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(homePage)),
      body: Center(
        child: TextButton(
          child: Text(detailsPage.toUpperCase()),
          onPressed: () => pushDetailsPage(context),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
class DetailsPage extends StatelessWidget
    with LoggerMixin, MyApplicationStrings {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(detailsPage)),
      body: Center(
        child: Column(
          children: [
            TextButton(
              onPressed: Navigator.of(context).pop,
              child: Text(goBack),
            ),
            TextButton(
              onPressed: () => logButtonPressed(example),
              child: Text(example),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

All of the examples provided can be found here on DartPad.

This is not a perfect example nor use case of each item shown. They are only created to introduce you to how mixins could be used. In fact, there are many more excellent ways to use them. Without careful use, they can become a hot mess.

Top comments (0)