<![CDATA[I should go to sleep]]>https://ishouldgotosleep.com/https://ishouldgotosleep.com/favicon.pngI should go to sleephttps://ishouldgotosleep.com/Ghost 4.1Sat, 19 Jun 2021 08:56:27 GMT60<![CDATA[This week in Flutter #8]]>Ever since I first watched this video from the Inkdrop developer Takuya Matsuyama, I wanted to try vim for Flutter. Robert Brunhage published a video on the topic, but I still have to find the right motivation (and time) to give it a try. If you have suggestions, comments, or

]]>
https://ishouldgotosleep.com/this-week-in-flutter-8/60cd9602d4d22d0001765e8aSat, 19 Jun 2021 08:50:56 GMTEver since I first watched this video from the Inkdrop developer Takuya Matsuyama, I wanted to try vim for Flutter. Robert Brunhage published a video on the topic, but I still have to find the right motivation (and time) to give it a try. If you have suggestions, comments, or even if you think I should not do it, please let me know.

- Michele Volpato

🧑‍💻 Development

Create a Simple Responsive GridView with Flutter

Alberto Bonacina shows us how to use flutter_staggered_grid_view to create a responsive grid of widgets, with an explanation of how the layout works.


Flutter Face Detection Using Firebase ML Kit

I have never used Firebase ML in an app. Apparently, it is quite simple. Follow this tutorial to implement face recognition in your app.


Flutter Package Preview: shared_preferences

Carl Wills is back with another introductory article to well-known Flutter packages. This time its shared_preferences.


Flutter GetX Example – Category Selection

Bart van Wezel is also back in this newsletter with an article about how to get started with GetX. There are so many state management packages for Flutter, I suggest you pick one and use that in all your projects. But it is good to know what's available out there, in case you find a package that works better for you.


Dart Basics

A new article by the raywenderlich.com team, about Dart, updated to the recent sound null safety feature.


Flutter Drag and Drop – State Management

Bart van Wezel is back (again) with an article about integrating drag and drop with Riverpod. There is a lot of code in this tutorial, and also a common questions section at the end.


Dependency Injection with Flutter

Dependency injection is a well-known technique to achieve separation of concerns, make your code reusable, and testable. In Dart/Flutter you can apply it by architecting your code around it from the start (my preferred way), or you can use catalyst for Dart and flutter_catalyst for Flutter. Julian Finkler, the developer behind those packages, created a new package to make the process of using them easier. From the repository of catalyst_builder:

Catalyst Builder is a dependency injection provider builder for both, Dart and Flutter. It's easy to use and dependency injection is almost done automatically. You only have to decorate your services with @Service and the build_runner will create a service provider for you.


🗄 Backend

Loading Cloud Storage Images in a Flutter Web App

Recently we adapted a Flutter mobile app we built for a client to the web. The app uses Firebase to host many images. In the mobile version, we cache the images in a folder on the device, using path_provider, but on the web, this is not possible. My colleague Toine Heuvelmans explains in this article how we used hive and CORS configurations to achieve image caching on the web.


🤷‍♂️ Others

A Year as a Flutter Developer

Do you want to read the journey of a Flutter developer, from learning to getting their first full-time contract? Alistair Holmes, co-organizer of Flutter Zimbabwe, shares how he learned Flutter, shared his work, and got a full-time contract (and then a Lead Mobile Dev position) in less than a year. Kudos!


10 Best Visual Studio Code Extensions for Flutter Development

I do not know if these are actually the best extentions for Flutter development, but for sure they are useful. Suresh Mohan not only lists them, but he also shows how they work in this article.


Improving Platform Channel Performance in Flutter

The Flutter team has recently improved performance in communication between the Flutter framework and the host platform. In this article, they explain (in detail - you are warned) how they achieved that. Happy reading.

]]>
<![CDATA[Extension methods for generic types]]>Since Dart 2.7 it is possible to add functionalities to existing libraries. If a class is missing a method you would like to use, you can just extend that class with your own implementation of such a method.

You do not need to create a pull request on the

]]>
https://ishouldgotosleep.com/extension-methods-for-generic-types/60c8d3f4d4d22d0001765e49Tue, 15 Jun 2021 16:34:15 GMTSince Dart 2.7 it is possible to add functionalities to existing libraries. If a class is missing a method you would like to use, you can just extend that class with your own implementation of such a method.

You do not need to create a pull request on the code of the library you want to update, you can keep the new functionality locally to your code. For example, if you need to format a Duration so that it shows days, hours, minutes, and seconds in the following format: dd:HH:MM:SS, you can define an extension method on Duration:

Extension on generic types

You can not only extend a "concrete" type like Duration. You can also extend the functionalities of a generic type, so that all its concrete instances (and all subclasses) obtain the new extension method.

For instance, if we want to add a firstWhereOrNull to List, where we either return the first element in the list that satisfies a condition, or null if none satisfies that condition, we can create an extension method like this:

By creating the extension method on the generic Iterable<E> we add it to all concrete types of all subclasses of Iterable, like Queue, List, and Set, thus also to List<String>.

]]>
<![CDATA[This week in Flutter #7]]>This week I attended the Flutter Netherlands meetup. At this online event, we discussed the announcements from the recent Google I/O, and met a special guest: Chris Sells. We were able to ask him questions and see how good he is in avoid answering some of them 😅.

Key

]]>
https://ishouldgotosleep.com/this-week-in-flutter-7/60c44c95d4d22d0001765d3fSat, 12 Jun 2021 07:10:05 GMTThis week I attended the Flutter Netherlands meetup. At this online event, we discussed the announcements from the recent Google I/O, and met a special guest: Chris Sells. We were able to ask him questions and see how good he is in avoid answering some of them 😅.

Key points from the event:

  1. The time between filing an issue in the Flutter repository and closing it (either by fixing or because it is not actionable) has been decreased by 80%. This has been possible by using Firebase testing and by keeping continuous integration up and running. If CI works well and is reliable, the fix can be delivered faster.
  2. Chris is proud that Flutter runs at the speed that the device allows, like a native app, and that the developers express joy with the features available for Flutter, like hot reload and the dev tools. They work a lot on making such features discoverable and easy to use.
  3. Flutter is used by more than 30 teams in Google, with more than a million lines of code. These teams depend on Flutter. It is impossible to stop it now. Do not expect to find Flutter in the Google graveyard soon.

On another note, the WWDC did not introduce many interesting new features, at least from a Flutter developer point of view. I am happy to see async await in Swift, but my hope for being able to use the iPad as a full development machine is lost. 😕

- Michele Volpato

🧑‍💻 Development

Flutter Crash Course

Robert Brunhage published a 30 minutes video where he shows how to create a simple net worth tracker app. A very good resource if you are just starting with Flutter.


Integration testing in Flutter

Recently, the integration_test package was promoted in the Flutter SDK. In two different articles, Shawn Blais and Darshan Kawar tell us why we should use it and how to use it. You should really try, there is something magical in seeing your UI being used by the automated tests.


Flutter localization done right

Jimmy Aumard introduces and explains his Flutter package, intl_flavors. It allows you to easily integrate different translations for different flavors of your Flutter app. I am not a big fan of flavors, but until Flutter adds support for them, for some cases they are unavoidable. The next time I cannot avoid them, I will use give this package a try.


Simplify your Flutter app with Provider

Carl Wills wrote an introductory article to state management and Provider. There are already plenty of articles out there about Provider, but I always welcome well-written articles for beginners, such as this one.

🗄 Backend

The @platform: a Firebase alternative

Joe Muller experimented with @protocol. He started from the challenge of implementing end-to-end encryption in a social media app built on top of Firebase. The @protocol is based on a unique digital identifier, the @sign, which lets you choose what data you want to share, with whom, and for how long. It reminds me (a lot) of the Solid web project. I think the technology is in a very early stage, but I really hope it will improve in the future, data control and ownership should be given back to the users.

]]>
<![CDATA[When to use a getter vs a method in Dart]]>Dart provides the possibility to use getters and setters, special methods that give read and write access to an object's properties.

You can use a getter to hide write access to a property:

enum ProductType { normal, offer }

class Product {
  // The product id.
  final String id = "ABC1234"
]]>
https://ishouldgotosleep.com/when-to-use-a-getter-vs-a-method-in-dart/60bc9352d4d22d0001765c90Sun, 06 Jun 2021 13:04:05 GMTDart provides the possibility to use getters and setters, special methods that give read and write access to an object's properties.

You can use a getter to hide write access to a property:

enum ProductType { normal, offer }

class Product {
  // The product id.
  final String id = "ABC1234";
  
  // This property cannot be modified directly from outside the object.
  ProductType _type;
  
  Product(this._type);
  
  // _type can be accessed read-only using this getter.
  ProductType get type => _type;
}

You can also use them to calculate a certain value that is not directly stored in your object:

  ...
  
  // Get whether the type of the product is an offer.
  bool get isOffer => _type == ProductType.offer;
 }

You could also use a method, instead of a getter:

  ...
  
  // Get whether the type of the product is an offer.
  bool isOffer() => _type == ProductType.offer;
 }

So, how do you know where to use a getter and where to use a method?

Personally, I avoid using a getter when the value cannot be calculated in O(1). The reason is that a getter and a property look the same from the outside, from the point of view of the user of your object.

final product = Product(ProductType.offer);

// Is this a getter or a property?
product.id;

// Is this a getter or a property?
product.isOffer;

So the fact that there is a calculation behind the getter might be lost for the final user of the object who might think that the getter is a property and will use it as a O(1) "function".

]]>
<![CDATA[This week in Flutter #6]]>Next week is WWDC time, and Apple might introduce new features in iOS, iPadOS, and macOS. Whether they will be available on Flutter depends on the type of feature.

Last year's biggest announcements were 1) widgets and 2) App Clips (I bet you forgot about them). The formers

]]>
https://ishouldgotosleep.com/this-week-in-flutter-6/60ba7904d4d22d0001765ba1Sat, 05 Jun 2021 06:56:32 GMTNext week is WWDC time, and Apple might introduce new features in iOS, iPadOS, and macOS. Whether they will be available on Flutter depends on the type of feature.

Last year's biggest announcements were 1) widgets and 2) App Clips (I bet you forgot about them). The formers are not available directly in Flutter, while there is a workaround for the latter, but with some size limitations.

I am looking forward to seeing what Apple has in store for developers this year. I am hoping for a way to effectively develop on iPad, now that there is an M1 version available.

- Michele Volpato

🧑‍💻 Development

Explore Streams And Sinks In Dart & Flutter

Shaiq Khan explains streams and sinks in Dart. If you are used to reactive programming, then nothing of this will be new for you. But if you are still learning it, you will definitely benefit from this article.


How To Validate Emails in Flutter

Luciano Jung is releasing a new article every week about how to use different published Flutter packages. This week is about email_validator. Although I do not use this package (yet), I thought it was worth mentioning, because I hope he sticks with this series of articles. I am always eager to learn about packages (and widgets) I do not know yet.


Do you know how to retry a Future in Flutter?

A simple and effective way to retry a Future that did was not successful, giving a maximum number of retries and a delay between them. By Vandad Nahavandipoor.


🤷‍♂️ Others

Book - Flutter Apprentice

raywenderlich.com announced a new book for developers who are getting started with Flutter. Their resources are always of good quality, but I did not have the opportunity to read this book yet. Buy at your own risk :)


🎤 Events

The Flutter Global Summit 2021 has been postponed to July 21-23. You can still register online. The "junior track" is free.

]]>
<![CDATA[This week in Flutter #5]]>Last week's Google I/O is still lingering, and more resources have been released, in particular on the Flutter YouTube channel. Furthermore, the Flutter Community has been posting new articles again.

🧑‍💻 Development

Building scrolling experiences in Flutter

Slivers can be used to add very neat

]]>
https://ishouldgotosleep.com/this-week-in-flutter-5/60b150a0dc15010001a99cc3Sat, 29 May 2021 09:05:08 GMTLast week's Google I/O is still lingering, and more resources have been released, in particular on the Flutter YouTube channel. Furthermore, the Flutter Community has been posting new articles again.

🧑‍💻 Development

Building scrolling experiences in Flutter

Slivers can be used to add very neat custom scrolling effects to your app. In this workshop, Kate Lovett shows how to use them to make your user experience much more interesting.


Let’s make the Flutter Navigator 2

I have been fighting recently with Flutter Navigation 2.0 a couple of weeks ago. If you want to save yourself a big headache, read this article by Ali Yazdi first, so you get an idea of what you are getting into.


just_audio tutorials

Suragch has been busy creating tutorials on how to get started with the just_audio package. How to play short audio clips, how to stream audio, and how to manage a playlist. I also wrote some tutorials on how to use just_audio, but I have to admit that they are not at Suragch's level.


Dart collections with DartX extensions

Anna Domashych shows us how to work with collections more elegantly, by using the dartx package. Really, why isn't that package just part of base Dart?


Creating a Cat Voting App with Flutter

Bart van Wezel had some fun creating a cat-voting app using the cat API. I like seeing funny projects like this one.


Improving Code Quality With Dart Code Metrics

Static code analysis won't necessarily improve your coding skills, but it certainly helps with keeping your coding style consistent within your organization.
Dart Code Metrics is a plugin for the Dart analyzer that reports code metrics, checks for anti-patterns, and provides additional static analysis rules. Dmitry Krutskikh talks about it in this article.

🧑‍🎨 Design

FlutterLogo (100th Widget of the Week!)

The Flutter YouTube channel reached the 100th widget of the week. You probably won't use this one, but it is a good reason for celebrating. 🎉


Colors / Numbers Game with Flutter Implicit Animations

Andrea is one of my favorite Flutter content creators. In this video, he shows a simple game that uses implicit animations. Andrea is also releasing a course about animations in Flutter. I haven't had the opportunity to follow the course yet, but it is on my to-do list, and once I do, I will let you know how it is.

🗄 Backend

Going full-stack with Flutter and Supabase - Part 2: Database

I already mentioned Supabase in a previous issue of this newsletter. In this article, Christos Giallouros goes deep into the database part of a Flutter/Supabase app. I like that using a relational database for app development has become as easy as using a Firestore instance.

🤷‍♂️ Others

Programming on your phone

We often assume that mobile developers have a computer at their disposal to create apps. Suragch has been tasked with teaching a mobile development class for students with no access to computers. This article reports the challenges faced during the evaluation of possible alternatives to use a computer for mobile development. While reading the article, I thought of an even simpler solution: 1. write the code and sync with GitHub, as in the article; 2. trigger a CI/CD build on Bitrise that deploys the app via Firebase App Distribution.
It will not be as fast as the proposed solution in the article because you lose instant feedback while you write code, but you do not need a server.

[The Bitrise link is a referral link].

Flutter in production: Stadia and Google Pay

Flutter is used in production by Google. In this interesting Q/A, Google engineers discuss their programming and design choices for Stadia and Google Pay.

]]>
<![CDATA[Take advantage of type aliases in Dart]]>https://ishouldgotosleep.com/take-advantage-of-type-aliases-in-dart/60abac14ed21a400017b7cfeMon, 24 May 2021 16:42:15 GMTDart 2.13 introduced type aliases for all types, not only function types. Like in the example provided in the official announcement, typedef Json = Map<String, dynamic>;,  type aliases provide syntactic sugar that helps the developing process.

Let's see how we can exploit this new feature of Dart, and how not to use it.

Mask generics

As for the Json example, we can specify a generic type the is used in the same way in our code. If everywhere in our code, when we handle json data, we expect it to be of type Map<String, dynamic>, then we can just type-define it as Json and just use it as a new type everywhere.

In a more complicated case, let's say we have an app where we sell our products and services. We created a PaymentService that triggers a payment event, based on the cart and a given payment method, like credit card, or bank transfer:

abstract class PaymentMethod {}
class CreditCard implements PaymentMethod {}
class BankTransfer implements PaymentMethod {}

class PaymentService<ItemType, Method extends PaymentMethod> {
  ...
}

We did this so that we can isolate credit card payment specific code in its specific class, and the same for bank transfer specific code. When we want to use PaymentService in our code, we specialize the generic type. For instance, a ShippingService might need to access payment services specialized on both credit card and bank transfer:

class ShippingService {
  final PaymentService<Product, CreditCard> creditCardPayment;
  final PaymentService<Product, BankTransfer> bankTransferPayment;
  ...
}

Those specialized generic types are always the same inside ShippingService, and they make the code harder to read. In this case we can define some easier-to-read types, one for CreditCardPayment and one for BankTransferPayment.

typedef CreditCardPayment = PaymentService<Product, CreditCard>;
typedef BankTransferPayment = PaymentService<Product, BankTransfer>;

We can now refactor ShippingService to a cleaner version:

class ShippingService {
  final CreditCardPayment creditCardPayment;
  final BankTransferPayment bankTransferPayment;
  ...
}

The detail implementation is now hidden in the new type name, but it can still be accessed by reviewing the typedef statements. We do not need to use the long specialized types anymore.

Better semantic

Another situation where type aliases can help us write better code is by giving the proper semantic to variables and class properties.

Let's say we define the dimensions of a product as

class Product {
  /// Height in centimetres.
  final double height;

  /// Length in centimetres.
  final double length;
  
  /// Width in centimetres.
  final double width;
  
  ...
}

There is nothing wrong here, but we can make it easier to read by writing some self-documenting code by using type aliases. We could define a Centimetres type alias:

typedef Centimetres = double;

class Product {
  final Centimetres height;
  final Centimetres length;
  final Centimetres width;
  
  ...
}

Now not only the type specifies the unit of measure, making the comments unnecessary, but if we find out that those dimensions should be expressed with an integer instead, we only need to change the alias.

Generic function type

The possibility to create an alias for a generic function is available in Dart since version 1.24.

A generic function type allows us to treat functions as first-class citizens, even from a readability point of view, using them everywhere a type is expected, for instance in type annotations, return types, and actual type arguments. It also helps by improving readability, which, as for long generic types, is a problem for complex function signatures.

Continuing the payment example above, let's say that we have a function that calculates the tax owed, given the products. We can define the type of such function as:

typedef CalculateTaxes<Product> = int Function(List<Product>);

void processPayment(List<Product> products, CalculateTaxes<Product> taxHandler) {
  ...
}

This change does not seem to improve a lot, but with more complicated function definitions, it could make a bigger change for code readability.

When not to use type aliases

Not all cases where we could use type aliases are good cases. If a type alias does not improve readability, and does not provide value, it should not be used.

For instance, if we use a list of products in our app, while the user is adding and removing products from this list, we can see it as the cart for that user.

typedef Cart = List<Product>;

But this Cart, as defined above, does not improve readability and does not add value. I would expect to be able to get the total of a cart by calling cart.total, but total is not available for List.

If we want to have a Cart type, we should consider creating a new class that contains a list of products, instead.

Conclusions

Type aliases are a very simple language feature that can be exploited to improve the readability of our codebase, but we should not over-use them, and we should consider carefully where a type alias is a good idea or just an over-complication.

]]>
<![CDATA[This week in Flutter #4]]>This week's Google I/O brought us many talks about Flutter. I really enjoyed Why null safety? by Bob Nystrom. It gives you a very good explanation of why adding (sound) null safety static analysis to Dart is valuable, affordable, and flexible while giving examples of some other

]]>
https://ishouldgotosleep.com/this-week-in-flutter-4/60a8b261ed21a400017b7bb1Sat, 22 May 2021 08:38:27 GMTThis week's Google I/O brought us many talks about Flutter. I really enjoyed Why null safety? by Bob Nystrom. It gives you a very good explanation of why adding (sound) null safety static analysis to Dart is valuable, affordable, and flexible while giving examples of some other static analysis techniques that are not. Now that Dart added null safety, inspired by Swift Optionals, it is time to get super-powered enums, also inspired by Swift. Will this just be a dream?

Did you miss the Google I/O event? read what’s new in Flutter 2.2, from the Flutter team.

🧑‍💻 Development

How It’s Made: I/O Photo Booth

Do you want to know how Very Good Ventures Team created the photo boot for Google I/O 2021? Well, follow the link. The article does not give you a step-by-step tutorial to recreate the app, but it explains what challenges they faced and how they approached the problem. That is way better than a step-by-step tutorial.


An In-Depth Dive Into Streaming Data Across Platform Channels on Flutter

Learn about interacting with native platform APIs from Dart. This should be an article every Flutter developer should read. You might never need to use platform channels yourself, but knowing how they work will improve your understanding of Flutter plugins.


All You Need to Know About Downloading Images in Flutter

Some days ago I was having problems understanding the loadingBuilder parameter of a Image.network. The day after Vandad Nahavandipoor published this video. One day too late. 😄


An Introduction to Dart

Mark Mahoney started a series of articles about the basics of Dart and Flutter. He also wrote a free book on the subject. I am always happy to see organized content like this. Well done.


Announcing type aliases

The Dart team announced type aliases, introduced in Dart 2.13. Now you can do

typedef Json = Map<String, dynamic>;

class User {
  final String name;
  final int age;
  
  User.fromJson(Json json) :
    name = json['name'],
    age = json['age'];
    
  Json get json => {
    'name': name,
    'age': age,
  };
}

Codebases will become a little bit clearer. Just do not start abusing them!

🧑‍🎨 Design

Custom Neumorphic Shapes in Flutter

I am not a big fan of neumorphism, but it looks like it might become more relevant in app design. Ali Rıza Reisoğlu shows us how to create your own custom shapes using flutter_neumorphic.

🗄 Backend

Cloud, Dart, and full-stack Flutter | Q&A

Amazing Ask Me Anything about integration between Flutter and Google Cloud. If you are into Dart "server-side" you must watch this. For instance, have a look at Dart Function Framework.

]]>
<![CDATA[This week in Flutter #3]]>🧑‍💻 Development

AngularDart will soon enter maintenance mode

The Dart team announced that they will soon release a stable external release of AngularDart with null safety, and then the project will only be maintained and not actively developed anymore. The reason is that the team will push Flutter

]]>
https://ishouldgotosleep.com/this-week-in-flutter-3/60a0de5b8692a500014abc70Sun, 16 May 2021 09:58:27 GMT🧑‍💻 Development

AngularDart will soon enter maintenance mode

The Dart team announced that they will soon release a stable external release of AngularDart with null safety, and then the project will only be maintained and not actively developed anymore. The reason is that the team will push Flutter for web more. More news at the Google I/O event.

Cleaner Flutter Vol. 7: Where the data comes from

Do you want to make your app data layer better? Then read this article from Marcos Sevilla, it is part of a series, so go read also the previous ones.

Run Flutter apps in multiple platforms parallelly.

I always run my apps in debug mode from the command line. I am an old-school developer. If you use Visual Studio Code, but I imagine it will be similar for other IDEs, you can debug your app easily from the editor itself. Minnu shows us how to run your app on multiple platforms simultaneously.

🧑‍🎨 Design

WhatsApp Clone (Backend & FrontEnd)

Balram Rathore is working on an amazing recreation of WhatsApp made in Flutter. With videos explaining the process. Available on GitHub and on YouTube.

🗄 Backend

Automatically publish a Flutter Web App on GitHub Pages

You can deploy your Flutter web app to Google Cloud, Amazon Web Service, or any other infrastructure as a service solution. But you can also just host it as a GitHub Pages website. See how in this article by Jan Mewes.

🤷‍♂️ Others

Q1 2021 user survey results

The Flutter team released the survey results about the reasons why developers adopt Flutter. I am not surprised by the first reason: a single codebase. In my experience, this is the main reason Flutter is chosen for clients' projects. Being able to make a single offer for both the Android and the iOS version of an app, which is lower than the combined offers for two distinct apps, is what resonates more with potential clients.
I am surprised to see "vision to expand to web and desktop" not at the second place. That is also a big selling point with the client: the possibility to provide a browser version of their app in the future, or even a desktop version (with a limited budget), helps selling Flutter even more.

Flutter development using Vim

Some developers hate it, many love it. Vim is used effectively for development for years. Robert Brunhage shows us how to increase your productivity when using Vim while developing in Flutter. Check also a more generic article about using Vim. Oh, and he is a Flutter & Dart Google Developer Expert with a very active YouTube channel, so go have a look.

]]>
<![CDATA[This week in Flutter #2]]>🧑‍💻 Development

What is the "lifecycle" of a Widget?

Is there a widget "lifecycle? Find out in this short video from the Flutter team.

Build a Flutter Wishlist App

There are plenty of tutorials available to get you started with a simple Flutter app. This

]]>
https://ishouldgotosleep.com/this-week-in-flutter-2/609594f35fa5e40001868c1bSat, 08 May 2021 13:22:02 GMT🧑‍💻 Development

What is the "lifecycle" of a Widget?

Is there a widget "lifecycle? Find out in this short video from the Flutter team.

Build a Flutter Wishlist App

There are plenty of tutorials available to get you started with a simple Flutter app. This is one of them. But it has the potential of getting very detailed and teach you a lot. You will build a wishlist app that (properly) authenticates using Auth0, and connects to a secure API. It is already better than the usual tutorial, which will use a fake login mechanism. From the team at Auth0.

🧑‍🎨 Design

React to Flutter: One Developers Journey

I love real-world stories about migrating or starting with Flutter. This is a small example of one by Reme Le Hane, describing a simple case of an animation that a new developer in the team managed to implement way quicker than what the team estimated. Keep such stories coming, Reme.

Interactive User Guidance or how to make a hole in the layout

An easy way to add a walkthrough for your app by adding an overlay to the scaffold of your page. Useful for that first-time users of your app. By Alex Melnyk.

🗄 Backend

30 Days of Appwrite

The team at Appwrite started a series of articles to teach you how to use the backend service. And they are using Flutter in those articles. If you follow the 30 days and build something awesome using their backend service you will be eligible for some swag and some Raspberry Pi 4 Dev Kits.

Flutter GraphQL: Quick start

Ever wanted to learn more about GraphQL and using it in Flutter? I did. This tutorial by Sean Connolly will get you on the right track.

]]>
<![CDATA[This week in Flutter #1]]>🧑‍💻 Development

OOP design patterns in Flutter

Mangirdas Kazlauskas started last year a series of articles about design patterns in Flutter. Each article describes a design pattern, as described in the literature, and also provides the implementation in Dart. I discovered this series only recently, and I jumped

]]>
https://ishouldgotosleep.com/this-week-in-flutter-1/609593965fa5e40001868c14Sat, 01 May 2021 19:23:00 GMT🧑‍💻 Development

OOP design patterns in Flutter

Mangirdas Kazlauskas started last year a series of articles about design patterns in Flutter. Each article describes a design pattern, as described in the literature, and also provides the implementation in Dart. I discovered this series only recently, and I jumped into it immediately.

You can also download the app code used in the articles.

Dart Data Class Generator for Visual Studio Code.

Not exactly recent, this extension allows you to generate Dart data classes easily, fast, avoiding boilerplate and external code generation. You can also automatically generate code for Equatable. This extension is already one of my favorites.

Tips and tricks for Flutter

Vandad Nahavandipoor is going fast with his tips and tricks for Flutter (and Dart). Every day he publishes one (or more, often more), code snippets with some small features of Flutter or Dart that you might not know about. Recently, I found interesting the Capturing Stack Traces in Dart Exceptions and the Calling Optional Functions in Dart. You should definitely subscribe to the repository feed.

Testing Mobx stores in Flutter

MobX is a well know state management library for JavaScript, based on functional reactive programming. Daniel Cardona Rojas guides you through writing unit tests for a Flutter application using it. This was a difficult article for me to follow, not being familiar with MobX. But it also made me want to try it out. Thanks, Daniel.

🧑‍🎨 Design

Adobe XD to Flutter v2.0

I have experimented with the Adobe XD plugin that generates Flutter code directly from your design. I must say that I was not that impressed with the result. But someone who is mainly a UI designer, and not a developer, might find it much more useful. The plugin is now updated for Flutter 2.

Dark Theme for Flutter Applications

I like simple articles. Not all developers know the basics about all the aspects of Flutter. One of them is how to handle different themes in your app. There are plenty of articles around about that, but I read this from Ali Rıza Reisoğlu very easy to understand.

🗄 Backend

Building a simple Grocery App in Flutter with Supabase

You learn every day something new. I did not know Supabase. Am I going to test it out soon? Of course! In the meanwhile learn about it with this article by Carlo Miguel Dy.

]]>
<![CDATA[Flutter full app 5. Add a local playlist]]>In the latest article, we started working on the architecture of the app, by adding Provider and moving hardcoded playlists in a temporary special service file.

In this article, we add a local playlist, with music by Brent Simmons available at inessential.com. We discover bugs (yes, we already have

]]>
https://ishouldgotosleep.com/flutter-full-app-5-add-a-local-playlist/6069dac371a9c80001131415Wed, 07 Apr 2021 17:36:50 GMTIn the latest article, we started working on the architecture of the app, by adding Provider and moving hardcoded playlists in a temporary special service file.

In this article, we add a local playlist, with music by Brent Simmons available at inessential.com. We discover bugs (yes, we already have bugs in the code), and we fix them. Finally, we change the behavior of the app when tapping one of the playlists, which will fix the usability issue explained in the previous article.

Add a local playlist

We start by adding a playlist to HardcodedPlaylistsService. First, we download some mp3 files:  Slow House, Vampire's Run, and Tie & Suit. We place them in a new folder: assets/audio/. The Flutter engine needs to know that we have some assets in that folder, so we add it to our pubspec.yaml:

...
flutter:
  assets:
      - assets/audio/
...

Now we can add a new playlist to HardcodedPlaylistsService:

final _inessential = [
    PlaylistItem(Author("Brent Simmons", null), "Slow House", null,
        Uri.parse("asset:///assets/audio/SlowHouse.mp3")),
    PlaylistItem(Author("Brent Simmons", null), "Vampire’s Run", null,
        Uri.parse("asset:///assets/audio/VampiresRun.mp3")),
    PlaylistItem(Author("Brent Simmons", null), "Tie & Suit", null,
        Uri.parse("asset:///assets/audio/TieSuit2021.mp3")),
  ];
  
@override
  Map<String, List<PlaylistItem>> get playlists {
    return {'Games': _gameSongs, "Inessential": _inessential};
  }

We also add a new field to PlaylistsService: playlists. You can see the implementation in the snippet above.

To get the playlist available in the app, we need to add the ListTile to CategorySelector:

ListTile(
  title: Text("Inessential"),
  onTap: () {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => Player(_audioPlayer,
            value.playlists['Inessential']!),
      ),
    );
  },
),
ListTile(
  title: Text("Games"),
  onTap: () {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => Player(
            _audioPlayer, value.playlists['Games']!),
      ),
    );
  },
),

Now if you run the app you can listen to the new playlist, but, in the console, you get an error:

The following ArgumentError was thrown resolving an image codec:
Invalid argument(s): No host specified in URI file:///null

This is because of a bug in Player: item.artworkUri.toString() returns the string null if artworkUri is null. We want to return a placeholder, so we temporary change the code to:

tag: AudioMetadata(
    title: item.title,
    artwork: item.artworkUri?.toString() ??
    	'https://via.placeholder.com/150'),

Now the app works without errors. But, while playing one playlist, to change to another playlist we need first to pause the current, playing one.

A more usable player

It is now time to refactor part of the app. The current player screen shows on top of the playlist that is currently playing. Thus if we are playing the games music and go back to select the inessential music, without pausing first, nothing will happen.

We need to change the screen shown after tapping a playlist on the main page to show the correct items, which are independent of the playlist that is currently playing.

A small refactor

First thing first, notice that AudioMetadata is not really needed. We can pass a PlaylistItem to the audio player. So we delete the audio_metadata.dart file and refactor PlaylistItem, Playlist, and Player:

// playlist_item.dart

/// An audio item
class PlaylistItem {
  /// The [Author] of this audio item.
  final Author author;

  /// The title of this audio item.
  final String title;

  /// The Uri to an image representing this audio item.
  final String artworkLocation;

  /// An Uri at which the audio can be found.
  final Uri itemLocation;

  PlaylistItem({
    required this.author,
    required this.title,
    this.artworkLocation = "https://via.placeholder.com/150",
    required this.itemLocation,
  });
}
/// playlist.dart

ListTile(
  selected: i == state.currentIndex,
  leading: Image.network(sequence[i].tag.artworkLocation), // changed
  title: Text(sequence[i].tag.title),
  onTap: () {
    _audioPlayer.seek(Duration.zero, index: i);
  },
),
/// player.dart

void _loadAudioSources(List<PlaylistItem> playlist) {
    _audioPlayer
        .setAudioSource(
      ConcatenatingAudioSource(
        children: playlist
            .map(
              (item) => AudioSource.uri(
                item.itemLocation,
                tag: item, // changed
              ),
            )
            .toList(),
      ),
    )
        .catchError((error) {
      // catch load errors: 404, invalid url ...
      print("An error occured $error");
    });
  }

Image.network uses String instead of Uri, so we refactored the artwork uri to a simple string.

We will also need to modify HardcodedPlaylistsService:

class HardcodedPlaylistsService implements PlaylistsService {
  final _gameSongs = [
    PlaylistItem(
        author: Author("Blizzard North", null),
        title: "Tristram",
        artworkLocation:
            "https://upload.wikimedia.org/wikipedia/en/3/3a/Diablo_Coverart.png",
        itemLocation: Uri.parse(
            "https://archive.org/download/IGM-V7/IGM%20-%20Vol.%207/25%20Diablo%20-%20Tristram%20%28Blizzard%29.mp3")),
    PlaylistItem(
        author: Author("Game Freak", null),
        title: "Cerulean City",
        artworkLocation:
            "https://upload.wikimedia.org/wikipedia/en/f/f1/Bulbasaur_pokemon_red.png",
        itemLocation: Uri.parse(
            "https://archive.org/download/igm-v8_202101/IGM%20-%20Vol.%208/15%20Pokemon%20Red%20-%20Cerulean%20City%20%28Game%20Freak%29.mp3")),
    PlaylistItem(
        author: Author("Lucasfilm Games", null),
        title: "The secret of Monkey Island - Introduction",
        artworkLocation:
            "https://upload.wikimedia.org/wikipedia/en/a/a8/The_Secret_of_Monkey_Island_artwork.jpg",
        itemLocation: Uri.parse(
            "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3")),
  ];

  final _inessential = [
    PlaylistItem(
        author: Author("Brent Simmons", null),
        title: "Slow House",
        itemLocation: Uri.parse("asset:///assets/audio/SlowHouse.mp3")),
    PlaylistItem(
        author: Author("Brent Simmons", null),
        title: "Vampire’s Run",
        itemLocation: Uri.parse("asset:///assets/audio/VampiresRun.mp3")),
    PlaylistItem(
        author: Author("Brent Simmons", null),
        title: "Tie & Suit",
        itemLocation: Uri.parse("asset:///assets/audio/TieSuit2021.mp3")),
  ];
  
  ...

A much bigger refactor

We are using the audio player from just_audio in many places. We could use Provider to get the player available wherever we need it. If in the future we want to add tests (we will), or if we want to swap just_audio out for a different package (we won't), it is a good idea now to hide the audio player behind an interface. When we are ready to add tests, we can create a mock that implements such an interface.

/// audio_player_service.dart

/// Enumerates the different processing states of a player.
enum AudioProcessingState {
  /// The player has not loaded an audio source.
  idle,

  /// The player is loading an audio source.
  loading,

  /// The player is buffering audio and unable to play.
  buffering,

  /// The player has enough audio buffered and is able to play.
  ready,

  /// The player is ready and playing.
  playing,

  /// The player has reached the end of the audio.
  completed,

  /// The status is unknown.
  unknown,
}

/// An enumeration of modes representing the loop status.
enum PlaylistLoopMode {
  /// No audio is looping.
  off,

  /// Looping the current audio.
  one,

  /// Looping the current playlist.
  all,
}

abstract class AudioPlayerService {
  /// Whether the player is playing any audio.
  Stream<bool> get isPlaying;

  /// Whether shuffle mode is currently enabled.
  Stream<bool> get shuffleModeEnabled;

  /// The current [AudioProcessingState] of the player.
  Stream<AudioProcessingState> get audioProcessingState;

  /// Which loop mode is currently active in the player.
  Stream<PlaylistLoopMode> get loopMode;

  /// Whether there is a previous audio in the playlist.
  ///
  /// Note: this account for shuffle and repeat modes.
  bool get hasPrevious;

  /// Whether there is a next audio in the playlist.
  ///
  /// Note: this account for shuffle and repeat modes.
  bool get hasNext;

  /// The current playlist of item.
  ///
  /// Note: this does not change with shuffle and repeat mode.
  Stream<List<PlaylistItem>?> get currentPlaylist;

  /// Skip to the previous audio in the playlist, if any.
  Future<void> seekToPrevious();

  /// Skip to the next audio in the playlist, if any.
  Future<void> seekToNext();

  /// Set a specific loop mode.
  Future<void> setLoopMode(PlaylistLoopMode mode);

  /// Set whether the shuffle mode is enabled.
  Future<void> setShuffleModeEnabled(bool enabled);

  /// Pause the player.
  Future<void> pause();

  /// Start playing from the item previously seeked to,
  /// or the first item if no seek was previously done.
  Future<void> play();

  /// Move to the start of the playlist.
  Future<void> seekToStart();

  /// Move to the `index` item in the playlist.
  Future<void> seekToIndex(int index);

  /// Load a playlist.
  ///
  /// Note: this is needed before playing any item.
  Future<Duration?> loadPlaylist(List<PlaylistItem> playlist);
}

AudioPlayerService is inspired by AudioPlayer in just_audio, but it is easy to use any other audio player package and adapt it to this interface.

The just_audio implementation is:

/// just_audio_player.dart

class JustAudioPlayer implements AudioPlayerService {
  final AudioPlayer _audioPlayer = AudioPlayer();

  // State

  @override
  Stream<AudioProcessingState> get audioProcessingState =>
      _audioPlayer.playerStateStream.map(
        (_playerStateMap),
      );

  @override
  Stream<List<PlaylistItem>?> get currentPlaylist =>
      _audioPlayer.sequenceStateStream.map(
        (sequenceState) {
          return sequenceState?.sequence
              .map(
                (source) => source.tag,
              )
              .whereType<PlaylistItem>()
              .toList();
        },
      );

  @override
  bool get hasNext => _audioPlayer.hasNext;

  @override
  bool get hasPrevious => _audioPlayer.hasPrevious;

  @override
  Stream<bool> get isPlaying => _audioPlayer.playingStream;

  @override
  Stream<PlaylistLoopMode> get loopMode =>
      _audioPlayer.loopModeStream.map((_loopModeMap));

  @override
  Stream<bool> get shuffleModeEnabled => _audioPlayer.shuffleModeEnabledStream;

  // Actions

  @override
  Future<void> pause() {
    return _audioPlayer.pause();
  }

  @override
  Future<void> play() {
    return _audioPlayer.play();
  }

  @override
  Future<void> seekToNext() {
    return _audioPlayer.seekToNext();
  }

  @override
  Future<void> seekToPrevious() {
    return _audioPlayer.seekToPrevious();
  }

  @override
  Future<void> setLoopMode(PlaylistLoopMode mode) {
    switch (mode) {
      case PlaylistLoopMode.off:
        return _audioPlayer.setLoopMode(LoopMode.off);
      case PlaylistLoopMode.one:
        return _audioPlayer.setLoopMode(LoopMode.one);
      case PlaylistLoopMode.all:
        return _audioPlayer.setLoopMode(LoopMode.all);
    }
  }

  @override
  Future<void> setShuffleModeEnabled(bool enabled) async {
    if (enabled) {
      await _audioPlayer.shuffle();
    }
    return _audioPlayer.setShuffleModeEnabled(enabled);
  }

  @override
  Future<void> seekToStart() {
    return _audioPlayer.seek(Duration.zero,
        index: _audioPlayer.effectiveIndices?.first);
  }

  @override
  Future<void> seekToIndex(int index) {
    return _audioPlayer.seek(Duration.zero, index: index);
  }

  @override
  Future<Duration?> loadPlaylist(List<PlaylistItem> playlist) {
    // TODO do not load a playlist if it is already loaded.
    return _audioPlayer
        .setAudioSource(
      ConcatenatingAudioSource(
        children: playlist
            .map(
              (item) => AudioSource.uri(
                item.itemLocation,
                tag: item,
              ),
            )
            .toList(),
      ),
    )
        .catchError((error) {
      // catch load errors: 404, invalid url ...
      print("An error occured $error");
    });
  }

  Future<void> dispose() {
    return _audioPlayer.dispose();
  }

  // Helpers

  static AudioProcessingState _playerStateMap(PlayerState? state) {
    final processingState = state?.processingState;
    if (processingState == null) return AudioProcessingState.unknown;
    switch (processingState) {
      case ProcessingState.idle:
        return AudioProcessingState.idle;
      case ProcessingState.loading:
        return AudioProcessingState.loading;
      case ProcessingState.buffering:
        return AudioProcessingState.buffering;
      case ProcessingState.ready:
        if (state?.playing ?? false)
          return AudioProcessingState.playing;
        else
          return AudioProcessingState.ready;
      case ProcessingState.completed:
        return AudioProcessingState.completed;
    }
  }

  static PlaylistLoopMode _loopModeMap(LoopMode mode) {
    switch (mode) {
      case LoopMode.off:
        return PlaylistLoopMode.off;
      case LoopMode.one:
        return PlaylistLoopMode.one;
      case LoopMode.all:
        return PlaylistLoopMode.all;
    }
  }
}

Now we can provide a JustAudioPlayer in main.dart. We also move the Providers around the MaterialApp because we want such objects to be available on all routes.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider<PlaylistsService>(
          create: (_) => HardcodedPlaylistsService(),
        ),
        Provider<AudioPlayerService>(
          create: (_) => JustAudioPlayer(),
          dispose: (_, value) {
            (value as JustAudioPlayer).dispose();
          },
        ),
      ],
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: CategorySelector(),
      ),
    );
  }
}

Note how we need to cast value to a JustAudioPlayer because we are actually providing a AudioPlayerService.

Next, we create a new widget that places its child at the top of the page and the audio controlling buttons at the bottom of the page. We can use this widget every time we want the buttons to be visible if some audio items are loaded.

/// player_buttons_container.dart

/// Widget that place the content of a screen on top of the buttons that
/// control the audio. `child` is wrapped in an [Expanded] widget.
class PlayerButtonsContainer extends StatelessWidget {
  final Widget child;

  PlayerButtonsContainer({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(child: child),
        Consumer<AudioPlayerService>(
          builder: (context, player, _) {
            return StreamBuilder<bool>(
              stream: player.audioProcessingState
                  .map((state) => state != AudioProcessingState.idle),
              builder: (context, snapshot) {
                // If no audio is loaded, do not show the controllers.
                if (snapshot.data ?? false)
                  return PlayerButtons();
                else
                  return Container();
              },
            );
          },
        ),
      ],
    );
  }
}

We are using state != AudioProcessingState.idle to control when to show the buttons. idle means that no audio has been loaded yet, which is a perfect example of when to hide the buttons.

Next is, finally, a screen that shows the content of a playlist. We will use the widget above to place the buttons at the bottom of the screen.

/// playlist_screen.dart

/// A screen with a playlist.
class PlaylistScreen extends StatelessWidget {
  final List<PlaylistItem> _playlist;

  PlaylistScreen(this._playlist, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: SafeArea(
          child: PlayerButtonsContainer(
            child: Playlist(_playlist),
          ),
        ),
      ),
    );
  }
}

Now we can have the CategorySelector screen navigating to the PlaylistScreen.

/// category_selector.dart

/// A selector screen for categories of audio.
///
/// Current categories are:
///  - all items;
class CategorySelector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: SafeArea(
          child: PlayerButtonsContainer(
            child: Consumer<PlaylistsService>(
              builder: (__, value, _) {
                return Column(
                  children: [
                    ListView(
                      shrinkWrap: true,
                      children: [
                        ListTile(
                          title: Text("All items"),
                          onTap: () {
                            Navigator.of(context).push(
                              MaterialPageRoute(
                                builder: (context) =>
                                    PlaylistScreen(value.allItems),
                              ),
                            );
                          },
                        ),
                      ]..addAll(
                          value.playlists.keys.map((playlistName) {
                            return ListTile(
                              title: Text(playlistName),
                              onTap: () {
                                Navigator.of(context).push(
                                  MaterialPageRoute(
                                    builder: (context) => PlaylistScreen(
                                        value.playlists[playlistName]!),
                                  ),
                                );
                              },
                            );
                          }),
                        ),
                    ),
                  ],
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

We wrap the content of this page in a PlayerButtonsContainer, and we remove the hardcoded playlist names, in favor of dynamically generating the ListTiles.

We are almost done. We need to modify Playlist to represent any playlist, not only the currently loaded one. Change the name to PlaylistView seems also appropriate.

/// playlist_view.dart

/// A list of tiles showing all the items of a playlist.
///
/// Items are displayed with a `ListTile` with a leading image (the
/// artwork), and the title of the item.
class PlaylistView extends StatelessWidget {
  final List<PlaylistItem> _playlist;

  PlaylistView(this._playlist, {Key? key}) : super(key: key);

  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (var i = 0; i < _playlist.length; i++)
          ListTile(
            // selected: i == state.currentIndex, // TODO only if this is the loaded playlist
            leading: Image.network(_playlist[i].artworkLocation),
            title: Text(_playlist[i].title),
            onTap: () {
              final player =
                  Provider.of<AudioPlayerService>(context, listen: false);

              player
                  .loadPlaylist(_playlist)
                  .then((_) => player.seekToIndex(i))
                  .then((_) => player.play());
            },
          ),
      ],
    );
  }
}

We do not have a way to know if one of the titles is being played, so we comment out the related line of code, for now.

Another class that should be refactored is PlayerButtons. It should use AudioPlayerService and not just_audio directly.

/// player_buttons.dart

/// A `Row` of buttons that interact with audio.
///
/// The order is: shuffle, previous, play/pause/restart, next, repeat.
class PlayerButtons extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AudioPlayerService>(builder: (_, player, __) {
      return Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          // Shuffle
          StreamBuilder<bool>(
            stream: player.shuffleModeEnabled,
            builder: (context, snapshot) {
              return _shuffleButton(context, snapshot.data ?? false, player);
            },
          ),
          // Previous
          StreamBuilder<List<PlaylistItem>?>(
            stream: player.currentPlaylist,
            builder: (_, __) {
              return _previousButton(player);
            },
          ),
          // Play/pause/restart
          StreamBuilder<AudioProcessingState>(
            stream: player.audioProcessingState,
            builder: (_, snapshot) {
              final playerState = snapshot.data ?? AudioProcessingState.unknown;
              return _playPauseButton(playerState, player);
            },
          ),
          // Next
          StreamBuilder<List<PlaylistItem>?>(
            stream: player.currentPlaylist,
            builder: (_, __) {
              return _nextButton(player);
            },
          ),
          // Repeat
          StreamBuilder<PlaylistLoopMode>(
            stream: player.loopMode,
            builder: (context, snapshot) {
              return _repeatButton(
                  context, snapshot.data ?? PlaylistLoopMode.off, player);
            },
          ),
        ],
      );
    });
  }

  /// A button that plays or pauses the audio.
  ///
  /// If the audio is playing, a pause button is shown.
  /// If the audio has finished playing, a restart button is shown.
  /// If the audio is paused, or not started yet, a play button is shown.
  /// If the audio is loading, a progress indicator is shown.
  Widget _playPauseButton(
      AudioProcessingState processingState, AudioPlayerService player) {
    if (processingState == AudioProcessingState.loading ||
        processingState == AudioProcessingState.buffering) {
      return Container(
        margin: EdgeInsets.all(8.0),
        width: 64.0,
        height: 64.0,
        child: CircularProgressIndicator(),
      );
    } else if (processingState == AudioProcessingState.ready) {
      return IconButton(
        icon: Icon(Icons.play_arrow),
        iconSize: 64.0,
        onPressed: player.play,
      );
    } else if (processingState != AudioProcessingState.completed) {
      return IconButton(
        icon: Icon(Icons.pause),
        iconSize: 64.0,
        onPressed: player.pause,
      );
    } else {
      return IconButton(
        icon: Icon(Icons.replay),
        iconSize: 64.0,
        onPressed: () => player.seekToStart(),
      );
    }
  }

  /// A shuffle button. Tapping it will either enabled or disable shuffle mode.
  Widget _shuffleButton(
      BuildContext context, bool isEnabled, AudioPlayerService player) {
    return IconButton(
      icon: isEnabled
          ? Icon(Icons.shuffle, color: Theme.of(context).accentColor)
          : Icon(Icons.shuffle),
      onPressed: () async {
        final enable = !isEnabled;
        await player.setShuffleModeEnabled(enable);
      },
    );
  }

  /// A previous button. Tapping it will seek to the previous audio in the list.
  Widget _previousButton(AudioPlayerService player) {
    return IconButton(
      icon: Icon(Icons.skip_previous),
      onPressed: player.hasPrevious ? player.seekToPrevious : null,
    );
  }

  /// A next button. Tapping it will seek to the next audio in the list.
  Widget _nextButton(AudioPlayerService player) {
    return IconButton(
      icon: Icon(Icons.skip_next),
      onPressed: player.hasNext ? player.seekToNext : null,
    );
  }

  /// A repeat button. Tapping it will cycle through not repeating, repeating
  /// the entire list, or repeat the current audio.
  Widget _repeatButton(BuildContext context, PlaylistLoopMode loopMode,
      AudioPlayerService player) {
    final icons = [
      Icon(Icons.repeat),
      Icon(Icons.repeat, color: Theme.of(context).accentColor),
      Icon(Icons.repeat_one, color: Theme.of(context).accentColor),
    ];
    const cycleModes = [
      PlaylistLoopMode.off,
      PlaylistLoopMode.all,
      PlaylistLoopMode.one,
    ];
    final index = cycleModes.indexOf(loopMode);
    return IconButton(
      icon: icons[index],
      onPressed: () {
        player.setLoopMode(
            cycleModes[(cycleModes.indexOf(loopMode) + 1) % cycleModes.length]);
      },
    );
  }
}

Now, finally, we can start playing a playlist, then open another playlist and play it without the need to first stop the previous one.

Quite a big change for a small bug, but we also refactored a lot of code to make it easier to maintain.

The code is, as usual, available on GitHub:

Release Add a local playlist, fix usability bug · mvolpato/the-player
Audio player app in Flutter. Created as a tutorial for learning Flutter. - mvolpato/the-player
]]>
<![CDATA[Flutter full app 4. Add Provider and move hardcoded data]]>https://ishouldgotosleep.com/flutter-full-app-4-add-provider-move-hardcoded-data/6060b609da230000017be58bMon, 29 Mar 2021 17:16:35 GMTIn the previous article, we did not add any new functionality to the code. We upgraded Flutter to version 2 and migrated to sound null safety.

In this article, we are going to add Provider to the app and move the hardcoded data in a class that will be used until we implement a way to get data from a real source. We will also add a new screen to the app.

Using Provider

Provider is one of my favorite packages for Flutter. It makes InheritedWidgets very convenient to use.

You use one of the providers made available by the package to inject an object in your widget tree, and then you either use Provider.of or Consumer to get that object lower in the tree.

When you use Provider.of or Consumer, you specify the type of the object you want to use, the class name, and you receive the object that is closer to where you are in the tree, injected with Provider.

Start with adding the dependency to pubspec.yaml:

# State management helper
provider: ^5.0.0

and then run flutter pub get in to download the package and add it to your project.

Create a service that exposes playlists

The idea is to use Provider to inject an object that exposes one or more playlists. We do not have yet a class that stores playlists, we do not even have a formal way to define the items of a playlist. We need to create a PlaylistItem class and an Author class that represents the author of a playlist item (the host of a podcast, or the artist of a song).

// author.dart
class Author {
  final String name;
  final Uri? image;

  Author(this.name, this.image);
}

// playlist_item.dart
import 'package:music_player/domain/playlists/author.dart';

class PlaylistItem {
  final Author author;
  final String title;
  final Uri? artworkUri;
  final Uri itemLocation;

  PlaylistItem(
    this.author,
    this.title,
    this.artworkUri,
    this.itemLocation,
  );
}

and now we can create a class that exposes lists of PlaylistItems.

// playlist_service.dart
import 'package:music_player/domain/playlists/author.dart';
import 'package:music_player/domain/playlists/playlist_item.dart';

abstract class PlaylistsService {
  List<PlaylistItem> get allItems;
  Map<Author, List<PlaylistItem>> get itemsByAuthor;
}

This class is abstract, so that we can write different kind of playlist providers. Until we decide how we are going to get data in the app, we use a hardcoded list of items:

// hardcoded_playlists_service.dart
import 'package:music_player/domain/playlists/author.dart';
import 'package:music_player/domain/playlists/playlist_item.dart';
import 'package:music_player/services/playlists/playlists_service.dart';

class HardcodedPlaylistsService implements PlaylistsService {
  final _gameSongs = [
    PlaylistItem(
        Author("Blizzard North", null),
        "Tristram",
        Uri.parse(
            "https://upload.wikimedia.org/wikipedia/en/3/3a/Diablo_Coverart.png"),
        Uri.parse(
            "https://archive.org/download/IGM-V7/IGM%20-%20Vol.%207/25%20Diablo%20-%20Tristram%20%28Blizzard%29.mp3")),
    PlaylistItem(
        Author("Game Freak", null),
        "Cerulean City",
        Uri.parse(
            "https://upload.wikimedia.org/wikipedia/en/f/f1/Bulbasaur_pokemon_red.png"),
        Uri.parse(
            "https://archive.org/download/igm-v8_202101/IGM%20-%20Vol.%208/15%20Pokemon%20Red%20-%20Cerulean%20City%20%28Game%20Freak%29.mp3")),
    PlaylistItem(
        Author("Lucasfilm Games", null),
        "The secret of Monkey Island - Introduction",
        Uri.parse(
            "https://upload.wikimedia.org/wikipedia/en/a/a8/The_Secret_of_Monkey_Island_artwork.jpg"),
        Uri.parse(
            "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3")),
  ];

  @override
  List<PlaylistItem> get allItems {
    return _gameSongs;
  }

  @override
  // TODO: implement itemsByAuthor
  Map<Author, List<PlaylistItem>> get itemsByAuthor =>
      throw UnimplementedError();
}

Inject the playlist service

Now we can inject an object of this class in the widget tree, in main.dart.

home: Provider<PlaylistsService>(
          create: (_) {
            return HardcodedPlaylistsService();
          },
          child: Player()),

We can use it in player.dart.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SafeArea(
          child: Consumer<PlaylistsService>(
            builder: (__, value, _) {
              _loadAudioSources(value.allItems);
              return Column(
                children: [
                  Expanded(child: Playlist(_audioPlayer)),
                  PlayerButtons(_audioPlayer),
                ],
              );
            },
          ),
        ),
      ),
    );

where _loadAudioSources(value.allItems) converts PlaylistItems into AudioSources, needed by just_audio, and loads them in the audio player.

void _loadAudioSources(List<PlaylistItem> playlist) {
    _audioPlayer
        .setAudioSource(
      ConcatenatingAudioSource(
        children: playlist
            .map(
              (item) => AudioSource.uri(
                item.itemLocation,
                tag: AudioMetadata(
                    title: item.title, artwork: item.artworkUri.toString()),
              ),
            )
            .toList(),
      ),
    )
        .catchError((error) {
      // catch load errors: 404, invalid url ...
      print("An error occured $error");
    });
  }

These changes can be found at this GitHub commit:

Add Provider and Plylist service · mvolpato/the-player@c8a6d04
Audio player app in Flutter. Created as a tutorial for learning Flutter. - mvolpato/the-player

Adding playlist selection

Next up is a change to the UI. We want to be able to select the list of audio we  play, and, in the future, add more playlists.

We create a new Widget, CategorySelector, and we take the state-related code from Player. This widget will show a list of playlists, and when you tap one, the app navigates to the Player widget.

/// category_selector.dart

class CategorySelector extends StatefulWidget {
  @override
  _CategorySelectorState createState() => _CategorySelectorState();
}

class _CategorySelectorState extends State<CategorySelector> {
  late AudioPlayer _audioPlayer;

  @override
  void initState() {
    super.initState();
    _audioPlayer = AudioPlayer();
  }

  @override
  void dispose() {
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: SafeArea(
          child: Consumer<PlaylistsService>(
            builder: (__, value, _) {
              return Column(
                children: [
                  Expanded(
                    child: ListView(
                      children: [
                        ListTile(
                          title: Text("All items"),
                          onTap: () {
                            Navigator.of(context).push(
                              MaterialPageRoute(
                                builder: (context) =>
                                    Player(_audioPlayer, value.allItems),
                              ),
                            );
                          },
                        ),
                      ],
                    ),
                  ),
                  StreamBuilder<bool>(
                      stream: _audioPlayer.playingStream,
                      builder: (context, snapshot) {
                        // If we are not playing, do not show the player buttons
                        if (snapshot.hasData && (snapshot.data ?? false))
                          return PlayerButtons(_audioPlayer);
                        else
                          return Container();
                      }),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

There is only one list of audio items, for now, so we only have one ListTile. On tapping it, we navigate to the Player widget where we can start playing the audio. We also add an AppBar to make navigation easier.

Player will need to be changed. We instantiate the audio player in CategorySelector, so we can pass it to Player, which now can be refactored into a StatelessWidget.

// player.dart

class Player extends StatelessWidget {
  final AudioPlayer _audioPlayer;
  final List<PlaylistItem> _playlist;

  Player(this._audioPlayer, this._playlist, {Key? key}) : super(key: key) {
    if (!_audioPlayer.playing) _loadAudioSources(_playlist);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: SafeArea(
          child: Column(
            children: [
              Expanded(child: Playlist(_audioPlayer)),
              PlayerButtons(_audioPlayer),
            ],
          ),
        ),
      ),
    );
  }

On constructing Player, we load the audio only if it is not playing. This is correct for the first playlist we play, but if we have more playlists, we cannot switch between them unless we stop the audio first. This is not a good user experience, and we will fix it in the future.

The app starts to look a little bit more interesting. In the next article, we will fix the user experience problem described above and we will add more playlists to the mix.

The full code for this article can be found on GitHub.

Release Add Provider and move hardcoded data · mvolpato/the-player
Audio player app in Flutter. Created as a tutorial for learning Flutter. - mvolpato/the-player
]]>
<![CDATA[Flutter full app 3. Update to Flutter 2, sound null safety, and add a license]]>https://ishouldgotosleep.com/update-flutter-2-null-safety-add-license/605f1979aba8710001105e2eFri, 12 Mar 2021 23:00:00 GMT
Flutter Dart just_audio
2.0.1 2.12.0 0.7.1

The Flutter team has recently announced Flutter 2. Among others, sound null safety is my favorite feature.

Today we are going to upgrade Flutter to version 2, and our project to sound null safety.

Upgrade Flutter

We start with upgrading Flutter:

flutter upgrade
flutter --version

Flutter 2.0.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c5a4b4029c (5 days ago) • 2021-03-04 09:47:48 -0800
Engine • revision 40441def69
Tools • Dart 2.12.0

This was painless, but now when we run the app in debug mode, we are greeted with a message: Running with unsound null safety. We need to visit the migration page from the Flutter team and follow the steps to migrate our project to sound null safety.

Migrate to sound null safety

Switch to the Dart 2.12 release

We are already on 2.12, as we can see from the output of flutter --version above. Just to be sure let's run dart --version.

Dart SDK version: 2.12.0 (stable) (Thu Feb 25 19:50:53 2021 +0100) on "macos_x64"

Check dependency status

Next, we run dart pub outdated --mode=null-safety, to check the dependency status. We depend only on just_audio.

Showing dependencies that are currently not opted in to null-safety.
[✗] indicates versions without null safety support.
[✓] indicates versions opting in to null safety.

Package Name  Current  Upgradable  Resolvable  Latest  

direct dependencies:
just_audio    ✗0.6.13  ✗0.6.15+1   ✓0.7.1      ✓0.7.1  

1 upgradable dependency is locked (in pubspec.lock) to an older version.
To update it, use `dart pub upgrade`.

1 dependency is constrained to a version that is older than a resolvable version.
To update it, edit pubspec.yaml, or run `dart pub upgrade --null-safety`.

As expected, we need to update just_audio. We should not blindly change the version in our pubspec.yaml. Let's check first what else changed:

Version Change
0.7.1
  • Fix IllegalSeekPositionException on Android (DenisShakinov).
  • Fix error playing files when a user agent is set.
0.7.0
  • Null safety.
  • Retype headers from Map to Map<String, String>
0.6.15+1
  • Fix doc references.
0.6.15
  • Fix bug with spaces in asset paths.
  • Fix bug setting a ClippingAudioSource after another source.
0.6.14+1
  • Update ICY metadata feature status in README.
0.6.14
  • Initial support for ICY metadata on iOS.
  • Upgrade to ExoPlayer 2.13.1 (MichealReed).

Nothing seems needing our attention. Our app is still so simple that it will unlikely break on a package upgrade.

We update the version of just_audio:

# Play music files
just_audio: ^0.7.1

and re-run dart pub outdated --mode=null-safety.

Showing dependencies that are currently not opted in to null-safety.
[✗] indicates versions without null safety support.
[✓] indicates versions opting in to null safety.

All your dependencies declare support for null-safety.

Migrate with the migration tool

Now we are ready for the migration, using the tool provided by the Flutter team.

dart migrate
See https://dart.dev/go/null-safety-migration for a migration guide.

Analyzing project...
[------------------------------------------------------------\]No analysis issues found.

Generating migration suggestions...
[-------------------------------------------------------------]

Compiling instrumentation information...
[-------------------------------------------------------------]

View the migration suggestions by visiting:

With a link to a webpage with the changes the tool made automatically:

The Flutter null safety migration tool

There are not many changes, but we want to perform the migration manually, to really understand what is happening.

Open pubspec.yaml and change the environment sdk:

environment:
  sdk: '>=2.12.0 <3.0.0'

We will get errors because the code is no sound nulls safe.

Open audio_metadata.dart and think about the field. We want the title to always be provided. About the artwork, we assume that sometimes it might not be available, but we want to have a default placeholder. Thus we change the constructor to:

// TODO change placeholder
AudioMetadata(
    {required this.title, this.artwork = 'https://via.placeholder.com/150'});

In player.dart the property _audioPlayer is not initialized in a constructor, but in initState(). We can mark it as late.

late AudioPlayer _audioPlayer;

In playlist.dart we need to change the type of the key in the constuctor.

const Playlist(this._audioPlayer, {Key? key}) : super(key: key);

It can be null, in fact, we do not use it in your app. We also need to update the StreamBuild generic type to SequenceState?, as per just_audio 0.7.1.

return StreamBuilder<SequenceState?>( // this was changed
      stream: _audioPlayer.sequenceStateStream,
      builder: (context, snapshot) {
        final state = snapshot.data;
        if (state == null) return CircularProgressIndicator(); // this was added
        final sequence = state.sequence; // this was changed
        return ListView(...)

Finally open player_buttons.dart. As for playlist.dart we need to update the type of the key:

const PlayerButtons(this._audioPlayer, {Key? key}) : super(key: key);

We need to update the generic type of the previous and next buttons' StreamBuilder.

StreamBuilder<SequenceState?>(...

We also need to change the parameter type of _playPauseButton to be nullable.

Widget _playPauseButton(PlayerState? playerState) {

The final change is to the onPressed function of the replay button:

return IconButton(
        icon: Icon(Icons.replay),
        iconSize: 64.0,
        onPressed: () => _audioPlayer.seek(Duration.zero,
            index: _audioPlayer.effectiveIndices?.first), // this was changed
      );

Now we are ready to run the app again 🤞...

💪 Running with sound null safety 💪

Yes, we did it.

Choose a license

If you want to distribute code on the Internet, like I am doing now with this series of articles, it is a good idea to think about a license. GitHub has a website to help you choose an open-source license, if you do not choose one, then the ordinary copyright law of your country applies.

I am not sure about how this series of articles will develop in the future, but in case it becomes a real app, I want to protect the code so that no other person can copy it and upload it to the stores.

For this reason, I am not going to use an open-source license, for now. You, as a reader, can read the code, download it, learn from it and run it on you machine with the purpose of learning. But you are not allowed to modify and/or use it in any project, commercial or non-commercial.

If you would like to re-use the code for one of your projects, any kind of projects, you can contact me, and we can discuss the specific use.

The current version of the code is available on GitHub.

In the next article we will add Provider and we will move the hardcoded data out of the audio player. We will also add the possibility to select different playlists.

]]>
<![CDATA[Flutter full app 2. Add a playlist to a simple music player in Flutter]]>https://ishouldgotosleep.com/repository-management-and-add-playlist/605f18e7aba8710001105e0bSat, 06 Mar 2021 23:00:00 GMT
Flutter Dart just_audio
1.22.6 2.10.5 0.6.13

In the previous article, we created a new project in Flutter and we added some buttons to interact with music playing in the app.

In this article we will start improving the repository where the code for the app is versioned, and we will add a playlist to the app.

Repository management

Keeping your code clean is not only about code itself but also about comments and documentation. It is common to have a README file where a new developer, or a future version of yourself, can get basic information about the project, how to install tools needed to work on it, how to tests it, and other information that is important to start programming on the right step.

We add a README.md file to the repository, for now we leave it empty, but we will add some information when needed.

Another common file is the CHANGELOG, which will track versions of the app paired with what was changed in each of them. A well-known way to maintain a changelog is Keep a Changelog, we follow it for this project.

// CHANGELOG.md

# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.1] - add basic buttons
### Added
- a row of buttons to play, skip, shuffle, and loop.

[0.0.1]: https://github.com/mvolpato/the-player/releases/tag/0.0.1

Create a playlist widget

In part 1 we created some controls to interact with the music playing in the app. We can skip to the next or the previous audio source, but there is no way to know what will play, or even what is now playing.

The playlist is an important part of a music player app. We will add one now.

To be able to display information about the audio sources, we need to add such information somewhere. The constructor AudioSource.uri that we used in player.dart has a parameter called tag that can be used in this case.

It is of type dynamic, because we can pass the information however we prefer. In our case, we create a new data class AudioMetadata that holds title and artwork of the audio source.

class AudioMetadata {
  final String title;
  final String artwork;

  AudioMetadata({this.title, this.artwork});
}

And we update player.dart with the title and the artwork.

_audioPlayer
    .setAudioSource(
  ConcatenatingAudioSource(
    children: [
      AudioSource.uri(
        Uri.parse(
            "https://archive.org/download/IGM-V7/IGM%20-%20Vol.%207/25%20Diablo%20-%20Tristram%20%28Blizzard%29.mp3"),
        tag: AudioMetadata(
          title: "Tristram",
          artwork:
              "https://upload.wikimedia.org/wikipedia/en/3/3a/Diablo_Coverart.png",
        ),
      ),
      AudioSource.uri(
        Uri.parse(
            "https://archive.org/download/igm-v8_202101/IGM%20-%20Vol.%208/15%20Pokemon%20Red%20-%20Cerulean%20City%20%28Game%20Freak%29.mp3"),
        tag: AudioMetadata(
          title: "Cerulean City",
          artwork:
              "https://upload.wikimedia.org/wikipedia/en/f/f1/Bulbasaur_pokemon_red.png",
        ),
      ),
      AudioSource.uri(
        Uri.parse(
            "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3"),
        tag: AudioMetadata(
          title: "The secret of Monkey Island - Introduction",
          artwork:
              "https://upload.wikimedia.org/wikipedia/en/a/a8/The_Secret_of_Monkey_Island_artwork.jpg",
        ),
      ),
    ],
  ),
)
    .catchError((error) {
  // catch load errors: 404, invalid url ...
  print("An error occured $error");
});

The playlist will show a list of audio sources, with the artwork as leading widgetand the title as main information. As with PlayerButtons, we create a new file in screens/commons because we might re-use the playlist in other parts of the app.

// playlist.dart

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';

class Playlist extends StatelessWidget {
  const Playlist(this._audioPlayer, {Key key}) : super(key: key);

  final AudioPlayer _audioPlayer;

  Widget build(BuildContext context) {
    return StreamBuilder<SequenceState>(
      stream: _audioPlayer.sequenceStateStream,
      builder: (context, snapshot) {
        final state = snapshot.data;
        final sequence = state?.sequence ?? [];
        return ListView(
          children: [
            for (var i = 0; i < sequence.length; i++)
              ListTile(
                selected: i == state.currentIndex,
                leading: Image.network(sequence[i].tag.artwork),
                title: Text(sequence[i].tag.title),
                onTap: () {
                // TODO: play this audio when tapped.
                },
              ),
          ],
        );
      },
    );
  }
}

As for the player buttons, we use a Stream exposed by the audio player: sequenceStateStream which encapsulates the current sequence of audio sources, in .sequence and the index representing the audio that is currently being played, in .currentIndex. We use a ListView to show the sequence of audio sources.

We mark the audio that is currently being played by selecting the related tile.

When one of the list tiles is tapped, we want the head of the player to move to the start of the related audio. If the player is already playing something, then it will start immediately to play the audio source of the tapped tile. We can achieve it with a method of AudioPlayer that we already used in the previous article: seek.

_audioPlayer.seek(Duration.zero, index: i);

Add the playlist widget to the app

With the new playlist widget, we can now add the list of audio sources to the app. We add a Column widget to the build method of Player, and the Playlist as the first child, while PlayerButtons as the second child of Column. We also wrap the Playlist in an Expanded widget, because we want it to take all the available spaceafter PlayerButtons is placed.

return Scaffold(
  body: Center(
    child: Column(
      children: [
        Expanded(child: Playlist(_audioPlayer)),
        PlayerButtons(_audioPlayer),
      ],
    ),
  ),
);

This works, but in some devices, like the iPhone 12, the UI falls outside of the safe area. We can fix it by wrapping the Column in a SafeArea widget.

return Scaffold(
  body: Center(
    child: SafeArea( // <-- added this
      child: Column(
        children: [
          Expanded(child: Playlist(_audioPlayer)),
          PlayerButtons(_audioPlayer),
        ],
      ),
    ),
  ),
);

And this is the result:

A playlist at the top of the screen

Keep the changelog updated

Now that we have a new working version of the app, we update the changelog.

## [0.0.2] - add playlist
### Added
- this changelog
- a playlist to the player

and we also update the version of the app in pubspec.yaml.

version: 0.0.2+1

Be a better citizen

We could be done for today. We added the playlist and the app works. But the code is not in a good shape, we have almost no comments.

When we come back to the code in a month, we will have forgotten most decisions we took. Comments are essential to help to remember them, or to help teammates getting started with the code we wrote.

Let's go back to all classes and add some documentation, after which this is the full code for this article.

// player.dart

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:music_player/domain/audio_metadata.dart';
import 'package:music_player/screens/commons/player_buttons.dart';
import 'package:music_player/screens/commons/playlist.dart';

/// An audio player.
///
/// At the bottom of the page there is [PlayerButtons], while the rest of the
/// page is filled with a [PLaylist] widget.
class Player extends StatefulWidget {
  @override
  _PlayerState createState() => _PlayerState();
}

class _PlayerState extends State<Player> {
  AudioPlayer _audioPlayer;

  @override
  void initState() {
    super.initState();
    _audioPlayer = AudioPlayer();

    // Hardcoded audio sources
    // TODO: Get sources with a network call, or at least move to a separated file.
    _audioPlayer
        .setAudioSource(
      ConcatenatingAudioSource(
        children: [
          AudioSource.uri(
            Uri.parse(
                "https://archive.org/download/IGM-V7/IGM%20-%20Vol.%207/25%20Diablo%20-%20Tristram%20%28Blizzard%29.mp3"),
            tag: AudioMetadata(
              title: "Tristram",
              artwork:
                  "https://upload.wikimedia.org/wikipedia/en/3/3a/Diablo_Coverart.png",
            ),
          ),
          AudioSource.uri(
            Uri.parse(
                "https://archive.org/download/igm-v8_202101/IGM%20-%20Vol.%208/15%20Pokemon%20Red%20-%20Cerulean%20City%20%28Game%20Freak%29.mp3"),
            tag: AudioMetadata(
              title: "Cerulean City",
              artwork:
                  "https://upload.wikimedia.org/wikipedia/en/f/f1/Bulbasaur_pokemon_red.png",
            ),
          ),
          AudioSource.uri(
            Uri.parse(
                "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3"),
            tag: AudioMetadata(
              title: "The secret of Monkey Island - Introduction",
              artwork:
                  "https://upload.wikimedia.org/wikipedia/en/a/a8/The_Secret_of_Monkey_Island_artwork.jpg",
            ),
          ),
        ],
      ),
    )
        .catchError((error) {
      // catch load errors: 404, invalid url ...
      print("An error occured $error");
    });
  }

  @override
  void dispose() {
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SafeArea(
          child: Column(
            children: [
              Expanded(child: Playlist(_audioPlayer)),
              PlayerButtons(_audioPlayer),
            ],
          ),
        ),
      ),
    );
  }
}
// player_buttons.dart

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';

/// A `Row` of buttons that interact with audio.
///
/// The order is: shuffle, previous, play/pause/restart, next, repeat.
class PlayerButtons extends StatelessWidget {
  const PlayerButtons(this._audioPlayer, {Key key}) : super(key: key);

  final AudioPlayer _audioPlayer;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        // Shuffle
        StreamBuilder<bool>(
          stream: _audioPlayer.shuffleModeEnabledStream,
          builder: (context, snapshot) {
            return _shuffleButton(context, snapshot.data ?? false);
          },
        ),
        // Previous
        StreamBuilder<SequenceState>(
          stream: _audioPlayer.sequenceStateStream,
          builder: (_, __) {
            return _previousButton();
          },
        ),
        // Play/pause/restart
        StreamBuilder<PlayerState>(
          stream: _audioPlayer.playerStateStream,
          builder: (_, snapshot) {
            final playerState = snapshot.data;
            return _playPauseButton(playerState);
          },
        ),
        // Next
        StreamBuilder<SequenceState>(
          stream: _audioPlayer.sequenceStateStream,
          builder: (_, __) {
            return _nextButton();
          },
        ),
        // Repeat
        StreamBuilder<LoopMode>(
          stream: _audioPlayer.loopModeStream,
          builder: (context, snapshot) {
            return _repeatButton(context, snapshot.data ?? LoopMode.off);
          },
        ),
      ],
    );
  }

  /// A button that plays or pauses the audio.
  ///
  /// If the audio is playing, a pause button is shown.
  /// If the audio has finished playing, a restart button is shown.
  /// If the audio is paused, or not started yet, a play button is shown.
  /// If the audio is loading, a progress indicator is shown.
  Widget _playPauseButton(PlayerState playerState) {
    final processingState = playerState?.processingState;
    if (processingState == ProcessingState.loading ||
        processingState == ProcessingState.buffering) {
      return Container(
        margin: EdgeInsets.all(8.0),
        width: 64.0,
        height: 64.0,
        child: CircularProgressIndicator(),
      );
    } else if (_audioPlayer.playing != true) {
      return IconButton(
        icon: Icon(Icons.play_arrow),
        iconSize: 64.0,
        onPressed: _audioPlayer.play,
      );
    } else if (processingState != ProcessingState.completed) {
      return IconButton(
        icon: Icon(Icons.pause),
        iconSize: 64.0,
        onPressed: _audioPlayer.pause,
      );
    } else {
      return IconButton(
        icon: Icon(Icons.replay),
        iconSize: 64.0,
        onPressed: () => _audioPlayer.seek(Duration.zero,
            index: _audioPlayer.effectiveIndices.first),
      );
    }
  }

  /// A shuffle button. Tapping it will either enabled or disable shuffle mode.
  Widget _shuffleButton(BuildContext context, bool isEnabled) {
    return IconButton(
      icon: isEnabled
          ? Icon(Icons.shuffle, color: Theme.of(context).accentColor)
          : Icon(Icons.shuffle),
      onPressed: () async {
        final enable = !isEnabled;
        if (enable) {
          await _audioPlayer.shuffle();
        }
        await _audioPlayer.setShuffleModeEnabled(enable);
      },
    );
  }

  /// A previous button. Tapping it will seek to the previous audio in the list.
  Widget _previousButton() {
    return IconButton(
      icon: Icon(Icons.skip_previous),
      onPressed: _audioPlayer.hasPrevious ? _audioPlayer.seekToPrevious : null,
    );
  }

  /// A next button. Tapping it will seek to the next audio in the list.
  Widget _nextButton() {
    return IconButton(
      icon: Icon(Icons.skip_next),
      onPressed: _audioPlayer.hasNext ? _audioPlayer.seekToNext : null,
    );
  }

  /// A repeat button. Tapping it will cycle through not repeating, repeating
  /// the entire list, or repeat the current audio.
  Widget _repeatButton(BuildContext context, LoopMode loopMode) {
    final icons = [
      Icon(Icons.repeat),
      Icon(Icons.repeat, color: Theme.of(context).accentColor),
      Icon(Icons.repeat_one, color: Theme.of(context).accentColor),
    ];
    const cycleModes = [
      LoopMode.off,
      LoopMode.all,
      LoopMode.one,
    ];
    final index = cycleModes.indexOf(loopMode);
    return IconButton(
      icon: icons[index],
      onPressed: () {
        _audioPlayer.setLoopMode(
            cycleModes[(cycleModes.indexOf(loopMode) + 1) % cycleModes.length]);
      },
    );
  }
}
// playlist.dart

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';

/// A list of tiles showing all the audio sources added to the audio player.
///
/// Audio sources are displayed with a `ListTile` with a leading image (the
/// artwork), and the title of the audio source.
class Playlist extends StatelessWidget {
  const Playlist(this._audioPlayer, {Key key}) : super(key: key);

  final AudioPlayer _audioPlayer;

  Widget build(BuildContext context) {
    return StreamBuilder<SequenceState>(
      stream: _audioPlayer.sequenceStateStream,
      builder: (context, snapshot) {
        final state = snapshot.data;
        final sequence = state?.sequence ?? [];
        return ListView(
          children: [
          for (var i = 0; i < sequence.length; i++)
            ListTile(
              selected: i == state.currentIndex,
              leading: Image.network(sequence[i].tag.artwork),
              title: Text(sequence[i].tag.title),
              onTap: () {
                _audioPlayer.seek(Duration.zero, index: i);
              },
            ),
        ],
        );
      },
    );
  }
}
// audio_metadata.dart

/// Represents information about an audio source.
class AudioMetadata {
  /// The name of the song/show/recording.
  final String title;

  /// URL to an image representing this audio source.
  final String artwork;

  AudioMetadata({this.title, this.artwork});
}

The app code is also available on GitHub.

In the next article of the series, we will migrate the project to sound null safety.

]]>