<![CDATA[ I should go to sleep ]]> https://ishouldgotosleep.com https://ishouldgotosleep.com/favicon.png I should go to sleep https://ishouldgotosleep.com Thu, 16 Sep 2021 19:32:49 +0200 60 <![CDATA[ This week in Flutter #20 ]]> https://ishouldgotosleep.com/this-week-in-flutter-20/ 613c5e2d2b4564000115228a Newsletter ]]> Sat, 11 Sep 2021 12:11:17 +0200 Big news this week! Google announced Dart 2.14 and Flutter 2.5: Apple Silicon support and standard lints are the prominent updates for Dart.

For Flutter there are performance improvements, Material You support, camera and image picker updates, Widget Inspector improvements, and a new starter project.

Read these recap by Motabar Javaid, Ozan Taskiran,

- Michele Volpato

Development ๐Ÿง‘โ€๐Ÿ’ป

๐Ÿ”— Flutter Performance Tips

Here is a short list of tips to improve the performance of your Flutter app. Each tip has a link to a longer explanation if you want more information about it. A nice list by Hasan Basri Bayat.

๐Ÿ”— Clean Network Layer in Flutter [Dio + Freezed + Json_Annotation]

Create your network layer once and use it in all your projects, with this easy-to-follow tutorial by Ercan Garip.

๐Ÿ”— Flutter Bloc: A Complete Guide

I stopped using the original BLoC some time ago, I find it too complicated to understand for new developers when they join the team. But if you want to learn more about it, Dhruv Nakum published a nice article where he creates a simple weather app.

๐Ÿ”— Using GraphQL with Flutter: A tutorial with examples

GraphQL is getting used more and more by API providers. There are some packages in Flutter/Dart that help you with using it in your app. I believe that graphql_flutter is one of the best and in this article, Chidume Nnamdi shows you how to use it with a tutorial.

๐Ÿ”— Cubit 101: What is It, How to Use it and More

And after the article about BLoC above, here is an article about Cubit, an evolution of BLoC. I always say that I stopped using BLoC, but I can see myself starting to use Cubit in the future. This is a well-written article, I suggest you read it even if you already know Cubit. Via Gianfranco Pigatto.

๐Ÿ”— Exploring the Stripe Flutter SDK

Stripe is a payment service system with good APIs. Emmanuel Etukudo shows you how you can integrate it into a Flutter app, with support with Apple Pay and Google Pay.

๐Ÿ”— Getting to know Flutter: Pull to refresh with online data

The pull to refresh is a common UX feature of apps with lists. Learn how to implement it with this article by TheOtherDev/s. This article uses an external package: pull_to_refresh, but...

๐Ÿ”— RefreshIndicator (Flutter Widget of the Week)

...you can also use the standard RefreshIndicator.

๐Ÿ”— Implementing SVG in Flutter with flutter_svg

Did you know you can easily use SVG in your Flutter app? Majid Hajian knows that, and he wrote an article about it.

๐Ÿ”— Advance Scrolling in Flutter - Part (1)

An extensive article about scrolling in Flutter, with examples and GIFs, so that you do not have to implement everything yourself to test it. By Dhruv Nakum.

Architecture ๐Ÿ›

๐Ÿ”— Flutter: MVVM architecture best practice using Provide & HTTP

MVVM, MVC, MVA, MVP, DDD, BYOB: there are many architectures you can follow when working on a mobile app. The best one is the one you feel most comfortable with. In this article, Maraj Hussain shows you an example of using MVVM in a Flutter project.

Tools ๐Ÿ› 

๐Ÿ”— Simplify test new features in Flutter app with debug_friend

Stanislav Ilin published a package to help you during the development of a Flutter app. The package is called debug_friend and it adds some views to your app giving information like device info, the possibility to delete app data, and viewing detail UI values, like sizes and borders. A real debug friend, indeed.

Backend ๐Ÿ—„

๐Ÿ”— Magic URL authentication with Flutter + Appwrite

Appwrite recently added Magic URL as a password-less authentication method. Learn how to implement it in your Flutter app with this tutorial by Damodar Lohani.

Others ๐Ÿคทโ€โ™‚๏ธ

๐Ÿ”— Add CarPlay to your Flutter App ๐Ÿš—

My first Flutter app was a simple screen showing whether there was an available parking spot in the company parking lot. If you were too late and all spots were taken, you had to park in a public parking lot down the street and walk back. The app saved you the time to check at the company facilities whether there was room for your car or not: if the app told you there was no room, you just went directly to the public parking space. A colleague asked me if we could have the app on CarPlay, but that was not possible at the time. Now, with the flutter_carplay package published by Oguzhan Atalay, we can try it again, once we are all back to the office.

]]>
<![CDATA[ This week in Flutter #19 ]]> https://ishouldgotosleep.com/this-week-in-flutter-19/ 61332215522680000161fdb4 Newsletter ]]> Sat, 04 Sep 2021 10:00:10 +0200 Some weeks there is not a lot of Flutter content published by the community. Some other weeks there is a lot of content out there. This week is one of the latter. My job is to select the best content for you, so let's get started right away: there is a lot to cover today.

- Michele Volpato

Development ๐Ÿง‘โ€๐Ÿ’ป

๐Ÿ”— Using the Flutter BLoC Pattern with AWS Amplify Datastore

You might not know it, but AWS has a tool that helps mobile developers creating backends for their app: Amplify. It comes with (beta) Flutter packages. In this article, Derek Bingham shows a simple implementation of the BLoC pattern that handles the state from Amplify DataStore.

๐Ÿ”— Building a Drawing App in Flutter

Learn about CustomPaint widgets and how to render and control a custom UI in Flutter with this well-written tutorial by Samarth Agarwal.

๐Ÿ”— Explore Animation in Flutter - PART 1

Adam Kif started a series of articles about animations in SwiftUI and Flutter. This is the Flutter one. If you are just getting started with animation, this is a good starting point.

๐Ÿ”— Flutter with the TomTom Maps APIs: Part 1

Developers at TomTom try to use Flutter with their Maps API. The result is this video and this article. It's nice to see bigger companies experimenting with Flutter.

๐Ÿ”— How to create Flutter charts with charts_flutter

There is a simple package, developed by Google, to draw charts in your Flutter app: charts_flutter. Learn how to use it with this article by Samson Omojola.
One of the projects I am working on will need charts and I am looking forward to applying what I learned from this article.

๐Ÿ”— How To Integrate Customized Google Map in Flutter

You can add Google Maps to your Flutter app, and it is quite easy. But what if you want to have a custom map in your app? In this detailed article, Jaimil Patel deep dives into customizing your Google Map in Flutter. There is a lot to read, so grab a snack before you start.

๐Ÿ”— Flutter Navigator 2.0 with BLoC: The Ultimate Guide

I still have problems with Navigation 2.0 in Flutter. More in detail, the interaction between the state of the app and removing/adding pages to the page stack. In this article, Jalal Addin Okbi integrates Navigator 2.0 and the BLoC pattern. I have abandoned BLoC more than a year ago, but if you are into it, then enjoy it.

Tools ๐Ÿ› 

๐Ÿ”— Flutter Version individual for every project - Flutter Version Manager

I am working on many Flutter projects. Most of them get updated to the latest stable Flutter version fairly soon after its release. One of them is still on Flutter 1, and, for many reasons, I don't see my team migrating it soon. This means I need at least two versions of Flutter on the same machine. I use aliases because I only need two versions. Aditya Agarwal explains how to use aliases or Flutter Version Manager.

๐Ÿ”— Flutter Integration Test for Android and IOS on Firebase Test Lab

When you work on a mobile app, testing can become a difficult task. In the office we have half a dozen devices, Android and iOS, we use to do exploratory testing, but in the last year and a half, we have been working from home most of the time. That is where cloud-based app testing services come in handy. Get started with Firebase Test Lab with this article by Benson Thew.

๐Ÿ”— Raster thread performance optimization tips

So you have a Flutter app that works. Its performance is fine, but it could be better. What do you do? You buy a faster smartphone. Or you follow this article by Filip Hracek from the Flutter team to learn how to find where the app is not performing well.

Backend ๐Ÿ—„

๐Ÿ”— Itโ€™s Here! Announcing Appwrite 0.10 and the new Realtime API!

Appwrite, the open-source backend server that winks at Firebase, added the Realtime API: a way to subscribe to your data to get updates as they happen. The Appwrite team treats Flutter as a first-class citizen, they release the Flutter SDK and documentation as fast as any other platform. Well done!

๐Ÿ”— Getting Started with Appwrite Realtime for Flutter

And after the announcement, a new tutorial popped up, by Damodar Lohani.

Others ๐Ÿคทโ€โ™‚๏ธ

๐Ÿ”— How to create Dart packages for Flutter

Have you ever published a Dart package? No? Me neither. If you want to know how to do it, check this article by Chidume Nnamdi.

๐Ÿ”— Increment - Mobile

Increment is a publication by Stripe, with issues touching topics like DevOps, Remote working, testing, etc. The latest issue is about mobile, with the article "How can mobile teams best use feature flags?" by Pooja Bhaumik, who is very active in the Flutter content creator community.

๐Ÿ”— When We Went to the DART Side

If you are an Android developer and you are worried about moving to Flutter, read this article by Shivarpit Sharma.

๐Ÿ”— Updating theming on an old app (The Boring Flutter Development Show, Ep. 51)

A new episode of The Boring Show is out. Do you ever go back to an old project and say "what was I even thinking when I wrote this?" This is exactly what happens all over again in this episode ๐Ÿ˜…. My favorite quote: "I am starting to think this app is not production-ready."

]]>
<![CDATA[ This week in Flutter #18 ]]> https://ishouldgotosleep.com/this-week-in-flutter-18/ 6129e630e80f9f0001f7b015 Newsletter ]]> Sat, 28 Aug 2021 10:55:50 +0200 Google Summer of Code is a way to help students get started with open source development. This year there were three Dart projects. One of them is a Flutter desktop app that helps developers choosing lint rules to apply to their projects.

Abdullah Deshmukh, the student working on the project, published an article about the project and his experience in the Summer of Code program.

I never attended a program like Summer of Code when I was a student. It looks like an amazing opportunity to learn from experienced people: you get assigned a mentor who will guide you during the process.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Flutter: How to draw a task completion ring with CustomPainter

Andrea Bizzotto publishes (another) lesson from his Flutter Animation Masterclass. In this one, he uses CustomerPainter to draw a "task completion" widget that looks a lot like a progress indicator.


Flutter: Animated Task Completion Ring with AnimationController and AnimatedBuilder

And right after the "task completion" widget, learn how to animate it with this related article.


How to create a custom Scrollbar using RenderShiftedBox

In the latest project I worked on, I had to implement a custom ScrollBar, but it wasn't as cool as the one Elina Safaryanova implemented in this article.


Flutter state management for purists

We are always eager to try new packages, and there are plenty of them that help us with state management. But what if we do not need them? Certainly for a small app or some sample code, using what is already available in the Flutter framework is enough. Inclu Cat on the Gentle Trail elaborates more on this subject in this article. He also mentions another article, from Suragch, called "Flutter state management for minimalists" which I also recommend reading.


๐Ÿคทโ€โ™‚๏ธ Others

Introducing package:flutter_lints

The next major release of Flutter (2.3.0) ships with a new package called flutter_lints. New projects will use it by default, deprecating the old linting rules which are "heavily outdated".


package:html (Flutter Package of the Week)

"Are you tired of Flutter's widget tree?"
"No."
"Are you feeling nostalgic for an HTML DOM?"
"DEFINITELY not."

- YouTube comment

Still package:html can be useful in many ways in your app. Learn more about it from the Flutter team.


]]>
<![CDATA[ This week in Flutter #17 ]]> https://ishouldgotosleep.com/this-week-in-flutter-17/ 6120afa5c956270001fae1f5 Newsletter ]]> Sat, 21 Aug 2021 12:39:53 +0200 I am not a big fan of Medium for tech articles. It is a closed platform, with a horrible user experience - try to copy and paste some terminal command in a medium article and let me know.

So I was pleased when I learned that Hashnode got (more) funding. Hashnode is a blogging platform for developers, like Dev.to. If you are starting a tech blog, and you do not want to host your own website then look at those two before going to Medium.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

An approach to error handling on Flutter

Rafael รquila shows a couple of ways to handle errors in a Dart program. He starts with the usual "print on catch" code, and then he provides the ResultWrapper<T> class that you can use to hold the error or the expected result. Finally, he also shows a functional approach using Dartz, a functional programming package for Dart.


How to Parse JSON in Dart/Flutter with Code Generation using Freezed

As a continuation of the article from last week, Andrea Bizzotto pushes JSON parsing to the limit with code generation. I am all for automating repetitive tasks, but I do not use code generation intensively. I find the needed annotations annoying, and not easy to understand for a new member of the team. I still use it sometimes, and when I use it I commit the generated files to the repository. Andrea argues that the code reviews become harder to perform, but you can easily exclude generated files from the merge diff.


The Ultimate Flutter Layout Guide

Ok, I am not sure this is the "ultimate" guide, but it is a good idea to keep it next to you when you really cannot understand why that container is not properly aligned/sized/visible. Thanks, Aditya Sharma.


Keys In Flutter - UniqueKey, ValueKey, ObjectKey, PageStorageKey, GlobalKey

In this comprehensive article, Dhruv Nakum talks about Keys in Flutter. We have seen keys in a previous issue, but Dhruv goes straight to the point in this article.


Creating a custom progress indicator

Get your hands dirty with some CustomPaint and create a new progress indicator with this article by Daniel Ko. Warning: there is math in there.


Flutter Timer vs Ticker: A Case Study

Andrea Bizzotto publishes a lesson from his Flutter Animation Masterclass, where he explains why you should use Ticker instead of Timer when building a stopwatch. I bought the course, and I must say that it is well done. If you are learning Flutter, and want to know more about animations, you should consider it. Just to be clear: I am not affiliated with Andrea and I paid full price for the course.


A guide to theming your app in Flutter

A quick and easy article about getting started with adaptive_theme and Riverpod to handle themes in your app, by Chinedu Imoh.


Master The Art of Dependency Injection ๐Ÿฑโ€๐Ÿ‘ค

Dependency injection is important to keep your code loosely coupled and highly testable. Are there special considerations about dependency injection when working on a Flutter app? We could have a very deep widget tree, forcing us to inject the dependency from the root to the leaf. Read more about dependency injection in Flutter from Dhruv Nakum.


๐Ÿ›  Tools

freeRASP

A tool like Crashlytics but for threats. You deploy it with your app, and it notifies you about attempts to clone it or reverse engineer it. I am not sure about what you can do after being notified. I consider the apps I distribute as at risk of being studied and cloned. You should not have secrets in the app, because it runs on a device you have no control of. Secrets should be managed only on platforms you fully control, like a backend server.


๐Ÿคทโ€โ™‚๏ธ Others

Flutter Hot Reload

If you want to know how Hot Reload works, and how it has been improved in Flutter 2.2, read this article by Jens Johansen.


Yellow underline text | Decoding Flutter

It happened to everybody. In your app, often in a text hero widget, your text has two yellow underlines. It happens when a Text widget does not have a TextStyle. Filip Hrรกฤek explains it to you and gives you a solution in this short video.

]]>
<![CDATA[ This week in Flutter #16 ]]> https://ishouldgotosleep.com/this-week-in-flutter-16/ 61177071c3edc30001809894 Newsletter ]]> Sat, 14 Aug 2021 11:36:01 +0200 GitHub is moving forward with its Codespaces tool. You can fire up a project from a GitHub repository in seconds and work on the code from a browser or Visual Studio Code.

Will it be possible to have your project running in a macOS environment stack? You do not need it for Flutter development, and Codespaces seems to be marketed to web developers.

Working on a Flutter app via browser from an iPad, preview the web version, and then trigger a test build on your CI to test the mobile versions does not sound smooth to me. What do you think? Would you like to be able to code your Flutter app from a browser?

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

How to Parse JSON in Dart/Flutter: The Essential Guide

Your app likely needs to get data from some APIs. Such data is probably provided in JSON format. Andrea Bizzotto shows you the basics of parsing JSON data in Dart.


Integrate Amazon Lex With Flutter App

Chatbots are becoming more and more important for the support strategy of a company. Amazon Lex is one of the tools you can use to provide such a feature in your app. Monikinderjit Singh has a simple tutorial to get it integrated with a Flutter app.


Subtle Flutter mistake I made today

When you share your code, you want to share your achievements, not your mistakes. That is why I like this small article by Indy Garcia. I must say that I made that mistake as well in the past, and I am sure you did as well.


๐Ÿคทโ€โ™‚๏ธ Others

Validating flutter at scale

If you tried to buy or sell a house in the Netherlands, then you know Funda. On their website, you find the majority of houses for sale, and you can contact the estate agent directly. In this article, the engineering team explains how they evaluated Flutter "for feature teams to build features simultaneously on web and mobile apps".


HeroMode (Flutter Widget of the Week)

HeroMode helps to disable the hero animation of a Hero widget. Why would you want to disable the animation of a Hero widget you added to the tree willingly? The most common case is when you have nested Navigators. You want the widget to animate only when the child Navigator is used.


History of Flutter

Are you curious about how Flutter was born and released? Tadas Petra published a short video telling you all the main events since the Sky Engine.


What can we do better to improve Flutter? โ€” Q2 2021 user survey results

The results for the quarterly survey from the Flutter team are out. Surprisingly, some developers are concerned about expanding Flutter to web and desktop apps. They would prefer the team to focus on mobile. My team and I were very happy with porting a mobile app to the web. The process was fairly painless, it saved time and money. I do not think we would have been able to release the web version in time if it was not for Flutter.

]]>
<![CDATA[ This week in Flutter #15 ]]> https://ishouldgotosleep.com/this-week-in-flutter-15/ 610e398d3a42cb00013d0610 Newsletter ]]> Sat, 07 Aug 2021 11:20:45 +0200 Microsoft is pushing development for the Surface Duo. They published a package for dual-screen apps. Personally, I do not see this kind of device becoming popular. ย also did not see taking pictures using a tablet becoming popular, but they keep adding cameras on the back of entry-level tablets, so my opinion does not count much ๐Ÿ˜….

In a small team, you need to make some compromises when you decide what to work on from the backlog, and if you have to choose between supporting dual-screens and, well, anything else, you often choose the latter. At least until Apple announces a dual screen tablet.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Flutter counter app, but using isolates

In Flutter, you can use *Isolates* to run code in parallel. If you need to perform some laborious task in the background, like searching inside a big zip file, then you can defer the computation to a separate thread by spawning an Isolate. In this short and simple article, Hrishikesh Pathak recreates the counter app using Isolates.


Side Effects in Flutter: What they are and how to avoid them

When you start with Flutter, you often end up adding a lot of code in the build method. For instance, you might fire a Future, or change the state. That leads to chaos. Andrea Bizzotto teaches you how to avoid such chaos. This is a must-read for new Flutter developers. Print the DOs and DONTs at the end of the article and stick them on the ceiling in your bedroom.


Adding Micro-Interactions With AnimatedSwitcher

Micro-interactions are those small animations that you do not notice, but that make the user experience so much better. They can be the reason why users keep or stop enjoying your app. Alejandro Ulate Fallas shows you how to get started with micro-interactions in your app.


๐Ÿ›  Tools

XD to Flutter v3.0

A new major version of XD to Flutter has been released, congratulations to Grant Skinner. I only used the first version of the plugin, and my team was evaluating the second version, as a prototyping tool our designer could use. Happy to see that the project is still active.

]]>
<![CDATA[ This week in Flutter #14 ]]> https://ishouldgotosleep.com/this-week-in-flutter-14/ 6105029d4d37620001966da0 Newsletter ]]> Sat, 31 Jul 2021 12:34:45 +0200 I had to make some changes to an iOS app written in Swift. I have been working mainly in Dart/Flutter for the last months, but I was surprised by how quickly I got back into Swift mode.

That makes me think of Swift as a very good language. I have the same with Dart. I do NOT have the same experience with JavaScript, but this is just my personal experience.

Which programming languages make you eager to use them, and which ones make you feel like you just want to run away?

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Social Authentication in Customized Flutter Applications

If there is something I learned about authenticating users in your apps, is that you should never build your own authentication service. It is too complicated. So complicated that there exist companies which business model is just to provide an authentication layer for you. All backend-as-a-service platforms give you an authentication service, as well. Another approach you can take is authenticating users with their existing social accounts.

In this article, Jaimil Patel shows you how to integrate social authentication in your Flutter app. Just remember that if you allow users to authenticate via Google or Facebook on iOS, you also must let them authenticate via Apple.


Responsive layouts in Flutter: Split View and Drawer Navigation

Andrea Bizzotto teaches us how to transform a Drawer into a split view when the screen has enough room for it. In the article, he creates a custom component SplitView and uses Riverpod to hold a global application state. The article is well written, and I particularly like that he touches on the concept of reusability of components. Tutorials are too often just a bunch of code that you can't reuse in your own projects.


How to implement a shake text effect in Flutter

Another article from Andrea Bizzotto. In this one, he shows us how to create a simple shake animation as feedback to user input. Pretty neat.


Flutter Hooks

There are different ways you can manage the state of your app in Flutter. You can use simple state management with stateful widgets, or you can use packages, like flutter_bloc, Provider, and Riverpod. Kefeh Collins claims that just a step above simple state management, there are Flutter hooks. Learn more about them in this article.


๐Ÿ›  Tools

Flutter UME

ByteDance, the company behind TikTok, released an in-app debug tool called UME. When you use it, as a developer dependency, in your app, you can see debug information directly in your app. ByteDance was featured in the latest Google I/O.


Managing multi-package Flutter projects with Melos

When you are working on a simple app, you have a single pubspec.yaml, and all your code is in a single lib folder. But if you are working on a much bigger project, you might need to keep code separated into different development packages, which increases the complexity of your project. Melos can make that simpler for you, and Sagar Suri shows us how.


๐Ÿคทโ€โ™‚๏ธ Others

Experimenting with Dart and Wasm

WebAssembly (Wasm) was designed with the idea of deploying high-performance applications on a web browser, but it can be also used in other apps.

Liam Appelbe and Michael Thomsen tell us about the interoperability between Dart and Wasm. Quite a technical article, I understood less than half of it ๐Ÿ˜….


640 Pages in 15 Months

Bob Nystrom is well known to the Dart/Flutter community. He is writing a book called Crafting Interpreters, and he used Dart to create a build system for the book. Read more about the challenges he encountered in this article.


Collection (Flutter Package of the Week)

I was following a course about image processing during my bachelor's. One of the assignments was to implement a lossless image compression program. I implemented my own version of the data structure used to hold the color information in memory and the memory usage exploded quite quickly.

Do not make my mistake, use existing libraries. The Collection library contains all those data structures you studied in your computer science course but never used.


]]>
<![CDATA[ This week in Flutter #13 ]]> https://ishouldgotosleep.com/this-week-in-flutter-13/ 60fbb7958ca9a40001b0bd98 Newsletter ]]> Sat, 24 Jul 2021 11:20:43 +0200 We have many resources about testing and some about Firebase Realtime Database. It is almost like content creators agree on the subject of the content they publish.

I give a lot of space to other developers in this newsletter so I think I deserve to share some of my achievements: this week I joined the Flutter team at raywenderlich.com as a tech editor ๐ŸŽ‰.

The content available at raywenderlich.com is always of high quality, so I am honored to be part of the team.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Better Bloc and Cubit Unit Testing

Did you ever have problems creating unit tests while using the BLoC pattern?
I surely did. This was one of the reasons why I am not using it anymore in the new apps I work on. Daniel Cardona Rojas comes to the rescue with this article, explaining his solution to the problem...


Generating Fake Data in Flutter using the Factory Pattern for Unit Testing

... and after you learn how to write tests, you might need to create fake data for such tests. Carlo Miguel Dy shows you how in this article.


Unit testing DateTime.now() with the help of Dart extensions

Using DateTime.now() in your code makes unit testing more difficult. You do not know how much time passed between the call to DateTime.now() and the moment you test its value, so you cannot compare timestamps. Reme Le Hane proposes a solution based on extending DateTime. I wonder if you can separate the two static methods into two different extensions, so that you can declare the setter only for testing, to avoid accidentally use it in your production code.

Another solution is to have a World service that you can inject into other classes. This service will provide values like DateTime.now() in production, or the real value for some sensors, or GPS location. While during testing it can be fully mocked.


Cat and Mice โ€“ Part 4: Implementing the AI

Bart van Wezel is continuing the implementation of his game with some artificial intelligence. He also implements some UI. The game is taking shape.


๐Ÿ—„ Backend

The Firebase Realtime Database and Flutter - Firecasts

Watch Todd Kerpelman in a new video about using Realtime Database with Flutter, finally clarifying (to me) when to use Realtime Database and when to use Firestore.


Firebase Realtime Database Tutorial for Flutter

Two good resources about Realtime Database and Flutter in the same week. Is this a coincidence? Yes, it is. If you prefer to read rather than watch the Firebase video, you can follow this tutorial by Vincenzo Guzzi.


๐Ÿง‘โ€๐ŸŽจ Design

Flutter for Single-Page Scrollable Websites with Navigator 2.0

In this series of articles, Cagatay Ulusoy shows different ways to create a single-page website using Flutter. You might think this is an easy topic, but a lot is going on and there are some choices you will need to make even before starting to code.


๐Ÿ›  Tools

Flutter Code Generation: Getting Started

We all used code generators in Flutter. Maybe to create some classes for a database we use in the app. But do we understand them?
In this article, Aachman Garg dives into what they are and how they work in Flutter.


๐Ÿคทโ€โ™‚๏ธ Others

Finding the importance of proper null safety migration (The Boring Flutter Development Show, Ep. 50)

At the end of the previous The Boring Flutter Development Show, Filip and Fitz encountered a bug. In this episode, they will find out what is happening.


Capturing a Flutter widget as an image using RepaintBoundary

This is an interesting article. What if you want to create an image from a widget in your app? For instance, you want the user to be able to share a piece of art created by them. Jordan Holland answers your questions.

]]>
<![CDATA[ This week in Flutter #12 ]]> https://ishouldgotosleep.com/this-week-in-flutter-12/ 60f2824ff3f66000015aca7e Newsletter ]]> Sat, 17 Jul 2021 11:28:51 +0200 Google hosted the Games Developers Summit 2021 this week. There is not much about Flutter, but some talks might be useful for Flutter developers as well. For instance, Personalizing your app in real-time: Powered by Firebase and ML, or Whatโ€™s new for Firebase LiveOps.

Let me know if there was some other talk that you found interesting.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Understand Constraints in Flutter

Do you ever wonder why sometimes you set the width of a container and nothing changes in your layout? That is because you did not understand constraints. Learn about constraints in Flutter in this article by Dany Tulumidis.


Getting Started With Flutter

Jonathan Sande updated the beginner tutorial about Flutter on raywenderlich.com to Flutter 2.2. The previous version was for Flutter 1.7, a bit outdated ๐Ÿ˜.


Flutter Tutorial - Keep Screen Awake [2021] Wakelock

Did you know you can prevent the device screen from going into sleep mode directly from your Flutter app? I did not. Here is a very short tutorial on how to do it by Johannes Milke.


Cat and Mice โ€“ Part 3: Implementing the Game

Bart van Wezel is back with the next iteration of his game built from scratch with Flutter. This week he implements the menu and the game logic. It is nice to follow the app being built, reading about the challenges he faces.


A deep and detailed article by Andrea Bizzotto on Flutter animations. In the article, he discusses implicit animations, tweens, explicit animations, and built-in explicit transition widgets. He also provides an app you can download to showcase all the animations shown in the article, and more. Definitely recomended.


The flutter โ€œconstโ€ Keyword demystified

We have all written

Padding(
    padding: EdgeInsets.all(16.0),
    child: ...
    ...

But should we be using const?

Padding(
    padding: const EdgeInsets.all(16.0),
    child: ...
    ...

In this article, Kefeh Collins teaches us about the const keyword, what it represents, and when we should use it.


๐Ÿง‘โ€๐ŸŽจ Design

How to use Google Fonts in a Flutter application

Chris Bongers shares how easy it is to use Google Fonts in your Flutter app. If you are a beginner, you should check Chris' website. He is learning Flutter and sharing daily something new he learns.


๐Ÿ›  Tools

Excluding Dart files from static analysis

Jordan Holland shows us how to get rid of all those problems that the static code analyser finds in generated code. You should not edit generated code, so you either avoid using code generating tools ๐Ÿ˜ข, or ย exclude the generated files from the analysis ๐Ÿฅณ.


๐Ÿคทโ€โ™‚๏ธ Others

Flutter Web: Should I use it? (Part 4โ€” I believe so)

Reme Le Hane concludes the Flutter Web: Should I use it? series with suggestions for developing an app that is for both mobile devices and web browsers. I am a big fan of reusing your code as much as possible. I keep all the code that can be shared in some shared widgets, and I use separated widgets when the code differs.


]]>
<![CDATA[ This week in Flutter #11 ]]> https://ishouldgotosleep.com/this-week-in-flutter-11/ 60e94a51f3f66000015ac91c Newsletter ]]> Sat, 10 Jul 2021 11:36:25 +0200 A couple of weeks ago Majid Hajian started a weekly video show about Flutter news. How did I miss it? It looks like I have a couple of videos to watch to catch up on.

I have noticed an increase in content about Flutter and Dart lately, both written and video. Even bigger players, like raywenderlich.com, are publishing more articles and tutorials.

It is good for the community to get more content, and, hopefully, one day we will get to the level of quantity and quality of other languages and frameworks. ๐Ÿคž

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Unlocking Your Flutter Widgets With Keys

Flutter uses keys to preserve state when you move around in your widget tree. In this article from raywenderlich.com, you will learn what keys are and how they work, when to use a key, and how to work with different types of keys.


๐Ÿ› Architecture

Cat and Mice โ€“ Part 2: Architecture

Bart van Wezel keeps going with the weekly series about creating a new Flutter app. This week he discusses the architecture of the app, starting from the navigation. He wonders if this architecture will also support authentication: read the article and give your opinion about it.


๐Ÿ—„ Backend

Announcing Appwrite 0.9: The open source Firebase alternative

Appwrite released a new version with support for Flutter desktop. Now you can register Linux, Windows, and Mac apps. ๐ŸŽ‰


๐Ÿง‘โ€๐ŸŽจ Design

Principles Of 3D Design For Flutter

In this article, Manas Pratap Thakur shows two things: how to use Matrix4 to transform a widget to give a 3D perspective, and how to fake a 3D image by changing the visible frame depending on the value of the explicit animation. The latter feels a bit clunky to me, but it can be a good starting point.


๐Ÿ›  Tools

Run Flutter tests with GitHub Actions

My CI/CD to go tool is Bitrise[1], but you can use any CI/CD tool of your choice to automatically run tests on your Flutter project. If, for instance, you want to run tests every time a new pull request is created in your GitHub project using GitHub actions, you can follow this easy tutorial by Jordan Holland.


  1. this is a referral link. โ†ฉ๏ธŽ


๐Ÿคทโ€โ™‚๏ธ Others

Flutter Web: Should I use it? (Part 3โ€Šโ€”โ€ŠOther considerations)

Reme Le Hane continues the Flutter Web: Should I use it? series with some considerations about whether Flutter is a good idea for your app depending on the project you are working on. From the platforms your app needs to be built for and the skills your team possesses. The next article will be the last one in the series, with suggestions for you, if you decide to use Flutter for your web app, stay tuned.


]]>
<![CDATA[ This week in Flutter #10 ]]> https://ishouldgotosleep.com/this-week-in-flutter-10/ 60e00e93f3f66000015ac71a Newsletter ]]> Sat, 03 Jul 2021 13:47:43 +0200 Recently GitHub (Microsoft) introduced Copilot, a tool that uses AI to write code. It uses OpenAI Codex, a system similar to GPT-3. If you did not understand a single word, do not worry. Watch this video from Mangirdas Kazlauskas to find out how it performs on Flutter.

Will we all be replaced by AI in the future? No idea, but they said similar things about Low Code/No Code, and that did not happen yet.

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

How to make your own native library bindings with Dart FFI

In this series of articles, Maksim Lin shares his experience in using Dart FFI to be able to playback audio on Linux. I am looking forward to the next articles in the series.


Working with Firebase Cloud Firestore made easier with "withConverter()"

In version 2.2.0 of the Firestore Flutter package, the Firebase team introduced withConverter, which allows interacting with collections/documents in a type-safe way. Chinmay Kabi shows us how to use it. I must say I added a TODO in the projects I am working on to use it there as well.


Exploring Flutter Navigator 2.0: VRouter

I have implemented Navigation 2.0 in a couple of apps for now, and I am not very happy with it: boilerplate code and increased complexity are my major concerns with it. VRouter might make it simpler. In this article, Naveen Srivastava shows us how to get started with it. Before using it you must take into account that it replaces MaterialApp.


Mixing packages that don't support null safety in Flutter

What do you do when you are migrating to null-safety, but there is just one package which is not updated yet? You do not migrate! Or you can use Jordan Holland's approach described in this article. I would go for not migrating yet. ๐Ÿ˜…


๐Ÿ—„ Backend

Building a Post Scheduling App with HarperDB & Flutter

This is Alberto Bonacina's article for the hackathon I advertised last week. I think this is my favorite so far. It is simple and useful. He might even publish the app in the stores.


๐Ÿง‘โ€๐ŸŽจ Design

Building Complex UI in Flutter: Magic 8-Ball

A new tutorial from raywenderlich.com. Create a 3D magic 8-ball.


Unbounded height / width | Decoding Flutter

You need to watch this video from the Flutter team. Filip Hrรกฤek, with the help of some funny animations, explains what happens when you get an "Unbounded height" error. One hundred pixels!


Creating adaptive layouts with Flutter

I have been experimenting with adaptive layouts in an app I am working on with my team. The app was initially only designed for iOS and Android, but recently the client requested a web version. Happy we went with Flutter from the beginning we had "only" to adapt the layout for bigger screens. Our work also improved the app on tablets. In this article, Tourรฉ Holder shows you how to use adaptive_layout to start from the beginning with different screen sizes in mind, so that you do not have to fix it later on.


๐Ÿ›  Tools

Cat and Mice โ€“ Part 1: CI/CD

Last week Bart van Wezel started a weekly series about creating a new Flutter app: design, implementation, publishing, and artificial intelligence. In this article, he sets up CI/CD using Codemagic.


๐Ÿคทโ€โ™‚๏ธ Others

Flutter Web: Should I use it? (Part 2โ€” Performance)

Reme Le Hane continues the article from last week with an article on the performance of a web app developed using Flutter. The results are not encouraging. We must say that the page speed insight is calculated on first load of your web app. After main.js has loaded, the website should get much faster.


How to build any kind of app in Flutter (and overcome Tutorial Hell)

We have all been there. You are learning a new topic and you watch tutorial after tutorial, course after course, and copy and paste code here and there. That is not how learning works. In this article, Andrea Bizzotto explains his workflow when he is learning a new topic. I would add one more state to his Learn and Practice feedback loop: try to explain the topic you are learning to someone who is not familiar with it. You can do that with an online article, or by giving a presentation to your colleagues during a recurring knowledge-sharing meeting.


Google I/O spotlight: Flutter in action at ByteDance

ByteDance got some spotlight during the latest Google I/O. In this interesting article, they share how they got Flutter into their daily workflow. The main point is: they needed to develop for multiple platforms. They also mention the valuable contributions they made to Flutter, reducing the app size, and removing janky UI. Another key point worth mentioning is that they realized it was not productive to re-implement existing products in Flutter, while it was advantageous to use it for new apps.


Top 51 Amazing ๐Ÿคฉ Resources to Learn Flutter & Dart

Are you just starting learning Flutter? Here is a big list of resources, put together by Kushal Goel, you can use to get up to speed. Just be careful not to get stuck in tutorial nightmare.

]]>
<![CDATA[ This week in Flutter #9 ]]> https://ishouldgotosleep.com/this-week-in-flutter-9/ 60d6d049324e4700019fdab7 Newsletter ]]> Sat, 26 Jun 2021 11:24:54 +0200 Flutter for web has been a game-changer for my team. When a client mentioned that they would like to have a web version of the app we had already built for them, we were glad we used Flutter. Soon we will be able to create desktop apps as well, but should we?

The desktop experience is much different from the mobile one. A feature like undo, is not common in mobile applications, but it is in desktop ones. Furthermore, you have a full keyboard available to you on desktop, will you make your desktop app working without a mouse?

What do you think?

- Michele Volpato

๐Ÿง‘โ€๐Ÿ’ป Development

Using Cubit for Managing States in Flutter

Bloc is a well-known library that helps with state management, but using the Bloc package can result in a lot of boilerplate. Cubit is a lighter version of Bloc. Neil Ruaro shows in a simple tutorial how to use it.


flutter_witai: ^0.0.1

Amey Sunu published a package to query Wit.ai, an API that helps with adding natural language experience to your products. The package is at an early stage, but I like to see a student being active in the open-source community.


Calling Native Libraries in Flutter with Dart FFI

I remember some years ago, I was working on an iOS app that used machine learning models to identify some specific sounds. The model, and the functions to use it, was in C code. It was a nightmare. Now, whit Dart FFI it would be much simpler.
This new tutorial on raywenderlich.com explains what Dart FFI is and shows how to use it to access native libraries that support C-interoperability.


Migrating an old app to Flutter 2

The Boring Flutter Development Show is back with a new episode. In this one, Filip and Fitz update the old Hacker News app to Flutter 2. Not without problems. When we migrated a relatively new app to Flutter 2, we did not use the sound null safety migration tool, we migrated manually, to keep everything under control.


How we make Flutter work with CallKit Call Directory

With CallKit you can integrate your calling services with other call-related apps. In this (long) article the authors of the flutter_callkit_voximplant package explain why they needed to create a new package, and how they implemented it. It is not common to read articles about how a package is conceived and implemented.


๐Ÿ—„ Backend

Announcing HarperDB Hackathon on Hashnode!

Hashnode, in collaboration with HarperDB, announced a hackathon. The goal is to build an app in your favorite language (Dart?) and framework (Flutter?), using the HarperDB backend service. You still have some days until the 30th of June, so hurry up.


๐Ÿคทโ€โ™‚๏ธ Others

Flutter Web: Should I use it? (Part 1โ€Šโ€”โ€ŠSEO)

Reme Le Hane gives his opinion on why SEO is not that important (for now) for Flutter web apps. I would add to his reasons that the usual Flutter web app does not need to be indexed by a search engine. Your content is probably behind a login screen, and if it is not, it is generated depending on some state the app is in. If your content is static, why using a web app? Just publish an HTML website. If you need your app to be discoverable by search engines, you could have a marketing website with a link to the login screen.


Version Management In Flutter

My team makes and maintains Flutter apps for several clients. Some of these apps are not yet migrated to Flutter 2. Some other apps are using Flutter 2. This means that we need two different versions of Flutter, or even more, on the same machine to maintain such apps. How do we do it? Flutter Version Manager. In this article, Naveen Srivastava shows us how to get started.


Dart the perfect CLI language

Ruby is not my favorite language. Let me rephrase it: Ruby is my second to last favorite language. When I need to work on a Fastlane file, I first check if one of my colleagues is online, hoping to unload the job to them (they read this newsletter, so now I am in trouble).
So why not using one of your preferred languages for command-line applications? Brett Sutton tells us why we should use Dart.

]]>
<![CDATA[ This week in Flutter #8 ]]> https://ishouldgotosleep.com/this-week-in-flutter-8/ 60cd9602d4d22d0001765e8a Newsletter ]]> Sat, 19 Jun 2021 10:50:56 +0200 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 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 ]]> https://ishouldgotosleep.com/extension-methods-for-generic-types/ 60c8d3f4d4d22d0001765e49 Dart, Tutorials ]]> Tue, 15 Jun 2021 18:34:15 +0200 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 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-7/ 60c44c95d4d22d0001765d3f Newsletter ]]> Sat, 12 Jun 2021 09:10:05 +0200 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 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 ]]> https://ishouldgotosleep.com/when-to-use-a-getter-vs-a-method-in-dart/ 60bc9352d4d22d0001765c90 Dart ]]> Sun, 06 Jun 2021 15:04:05 +0200 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";
  
  // 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-6/ 60ba7904d4d22d0001765ba1 Newsletter ]]> Sat, 05 Jun 2021 08:56:32 +0200 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 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-5/ 60b150a0dc15010001a99cc3 Newsletter ]]> Sat, 29 May 2021 11:05:08 +0200 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 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/ 60abac14ed21a400017b7cfe Dart ]]> Mon, 24 May 2021 18:42:15 +0200 Dart 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-4/ 60a8b261ed21a400017b7bb1 Newsletter ]]> Sat, 22 May 2021 10:38:27 +0200 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 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-3/ 60a0de5b8692a500014abc70 Newsletter ]]> Sun, 16 May 2021 11:58:27 +0200 ๐Ÿง‘โ€๐Ÿ’ป 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-2/ 609594f35fa5e40001868c1b Newsletter ]]> Sat, 08 May 2021 15:22:02 +0200 ๐Ÿง‘โ€๐Ÿ’ป 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 ]]> https://ishouldgotosleep.com/this-week-in-flutter-1/ 609593965fa5e40001868c14 Newsletter ]]> Sat, 01 May 2021 21:23:00 +0200 ๐Ÿง‘โ€๐Ÿ’ป 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[ Hero widget working only when going back in Flutter ]]> https://ishouldgotosleep.com/hero-widget-working-only-when-going-back-in-flutter/ 607dcba06e2bea00010996fe Tips ]]> Mon, 19 Apr 2021 22:44:26 +0200 I stumbled across a weird bug in one of the apps I am working on: the hero animation was not visible on navigating to a new route, but it was present when popping the route to go back to the initial page.

I checked that the Hero widget was set up properly. The tag was the same, the widget subtree was exactly the same, below the Hero widget, and I was using MaterialApp, which makes the Hero transition work automagically.

It turned out that an ancestor of the Hero was a StreamBuilder, and it had no initialData. As explained in the documentation, the framework first calculates where the Hero will be in the destination route, but it does that with the initialData of the StreamBuilder, which, if not provided, is null.

So, code like this, in the destination page, does not work:

StreamBuilder<String>(
      stream: imageService.url,
      builder: (context, snapshot) {
        final url = snapshot.data;
        if (url == null)
          return CircularProgressIndicator();
        else
          return Hero(
              tag: "image",
              Image.network(url),
          ); 
      }
),

because, when the framework checks the position of the Hero in the new screen, the StreamBuilder as an initial data of null, and the progress indicator should be shown, not the Image widget.

When we go back to the source screen from the destination one, the framework already knows the emitted value for the previous screen, and the Hero animation is shown correctly.

The solution is to add initialData to the StreamBuilder:

StreamBuilder<String>(
 	  initialData: defaultUrl,
      stream: imageService.url,
      builder: (context, snapshot) {
        final url = snapshot.data;
        if (url == null)
          return CircularProgressIndicator();
        else
          return Hero(
              tag: "image",
              Image.network(url),
          ); 
      }
),

where defaultUrl must either be a placeholder value or is presumably obtained synchronously higher in the widget tree.

Now the Hero animation works both when pushing the destination route and when popping it.

]]>
<![CDATA[ Flutter full app 5. Add a local playlist ]]> https://ishouldgotosleep.com/flutter-full-app-5-add-a-local-playlist/ 6069dac371a9c80001131415 Flutter, Tutorials ]]> Wed, 07 Apr 2021 19:36:50 +0200 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 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/ 6060b609da230000017be58b Flutter, Tutorials ]]> Mon, 29 Mar 2021 19:16:35 +0200 In 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[ Convert all SVG files in a folder to PNG ]]> https://ishouldgotosleep.com/convert-all-svg-files-in-a-folder-to-png/ 605f6f9f444cfe00016c5fcd Tips ]]> Sat, 27 Mar 2021 18:50:43 +0100 I changed the way I create posts in this website. The new way automates most of the boring tasks.

One downside is that it does not work well with SVG files.

I had to convert all the SVG files in my site to PNG.

First I installed inkscape:

brew install inkscape

Then I run this command from the folder where all my SVG files are:

inkscape --export-type="png" $(ls -q)
]]>
<![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/ 605f1979aba8710001105e2e Flutter, Tutorials ]]> Sat, 13 Mar 2021 00:00:00 +0100
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/ 605f18e7aba8710001105e0b Flutter, Tutorials ]]> Sun, 07 Mar 2021 00:00:00 +0100
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.

]]>
<![CDATA[ Flutter full app 1. Music Player: create a simple Flutter music player app ]]> https://ishouldgotosleep.com/simple-flutter-music-player-app/ 605f17bfaba8710001105dd9 Flutter, Tutorials ]]> Fri, 19 Feb 2021 00:00:00 +0100
Flutter Dart just_audio
1.22.6 2.10.5 0.6.13

Today I decided to start a little project to show how to create a full app in Flutter, from an initial proof of concept, to testing, to improving UX and UI, to (light) project management. Hopefully, the app will be integrated with additional features as I publish more articles.

The idea is to create an audio (music/podcast) player that you can use to play audio from the internet. In this first article, we start by using just_audio, a package for playing audio by Ryan Heise.

Setup the project

We start by creating the Flutter project with

flutter create music_player

then we change the app version, in pubspec.yaml from 1.0.0+1 to 0.0.1+1, because we will use 1.0.0 for the first full release. While we are in pubspec.yaml we can also add the just_audio package dependency:

  # Play music files
  just_audio: ^0.6.12

Add a play button

Now it is time to start with coding. The very first goal is to have a button that, when pressed, starts playing music. So, create a new folder in lib, called screens. This is the folder where most of the files related to the UI will reside. In screens create a Dart file called player.dart. Create a new stateful widget, with a private property called _audioPlayer of type AudioPlayer, which is defined in just_audio.

In void iniState() we are going to initialize the audio player, and in void dispose()we are going to dispose of it, as per documentation.

So this is the code up until now:

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

class Player extends StatefulWidget {
  @override
  _PlayerState createState() => _PlayerState();
}

class _PlayerState extends State<Player> {
  AudioPlayer _audioPlayer;

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

    // Set a sequence of audio sources that will be played by the audio player.
    _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")),
      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")),
      AudioSource.uri(Uri.parse(
          "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3")),
    ]))
        .catchError((error) {
      // catch load errors: 404, invalid url ...
      print("An error occured $error");
    });
  }

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

An AudioSource can be a remote file, like in the code above, or a local file.

We need a button to be able to start and stop the audio player. We are going to use a method to discriminate between the different states of the app: playing, paused, completed.

Create a private method Widget _playerButton(PlayerState playerState).

PlayerState is a class defined by just_audio that holds information about, you guess it, the state of the audio player. More in details it

/// Encapsulates the playing and processing states. These two states vary
/// orthogonally, and so if [processingState] is [ProcessingState.buffering],
/// you can check [playing] to determine whether the buffering occurred while
/// the player was playing or while the player was paused.

This is the code we are going to add to it.

   Widget _playerButton(PlayerState playerState) {

    // 1
    final processingState = playerState?.processingState;
    if (processingState == ProcessingState.loading ||
        processingState == ProcessingState.buffering) {
        
      // 2
      return Container(
        margin: EdgeInsets.all(8.0),
        width: 64.0,
        height: 64.0,
        child: CircularProgressIndicator(),
      );
    } else if (_audioPlayer.playing != true) {
    
      // 3
      return IconButton(
        icon: Icon(Icons.play_arrow),
        iconSize: 64.0,
        onPressed: _audioPlayer.play,
      );
    } else if (processingState != ProcessingState.completed) {
    
      // 4
      return IconButton(
        icon: Icon(Icons.pause),
        iconSize: 64.0,
        onPressed: _audioPlayer.pause,
      );
    } else {
    
      // 5
      return IconButton(
        icon: Icon(Icons.replay),
        iconSize: 64.0,
        onPressed: () => _audioPlayer.seek(Duration.zero,
            index: _audioPlayer.effectiveIndices.first),
      );
    }
  }
  1. extracts the processing state, which is one among idle, loading, buffering, ready, and completed;
  2. if the player is in a temporary state, like loading and buffering, the button is replaced by a loading indicator;
  3. otherwise, if the audio player is not playing anything, which means that it is either paused or not started yet, the button is a play button that invokes the play()method on the audio player;
  4. otherwise, if the state is not completed, then the audio player is playing one of the audio sources and the button is a pause button that invokes pause() on the audio player;
  5. finally, if all the above are not true, then the player has finished playing the sequence of audio sources, and it is still playing nothing, and the button is a replay button that moves the head of the player to the very beginning of the first audio source. Given that the player is playing, but it reached the end of the sequence, the music will start immediately, without the need to invoke an additional play().

The only thing missing now is the build method:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: StreamBuilder<PlayerState>(
        stream: _audioPlayer.playerStateStream,
        builder: (context, snapshot) {
          final playerState = snapshot.data;
          return _playerButton(playerState);
        },
      ),
    ),
  );
}

just_audio provides some handy streams to emit the state and data to listeners. One of them is playerStateStream, which emits PlayerState objects, just what is needed in _playerButton. A StreamBuilder is the perfect widget to handle the state and build the button.

Now we need to refer to this widget in main.dart.

// main.dart

import 'package:flutter/material.dart';
import 'package:music_player/screens/player.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Player(),
    );
  }
}

Add other buttons

Run the app, and try it. It works. But it is a bit sad: just a button in the middle of the screen.

Just a play button in the middle of the screen

We will need to add more buttons to make it more interesting. First of all, we are going to separate the buttons UI from the player screen. We might want to use the same UI in some other screens in the future.

Create a new folder in screens called commons, and add in it a new file called player_buttons.dart. The widget in this file is a StatelessWidget, because it will use streams to handle the state.

Player needed to be stateful because it needs to handle initialization and disposalof the audio player, but in this widget we can just inject the player.

In the future, we are going to change how we initialize and dispose of the audio player, and how we inject it into those widgets that need it, but for now, this is enough.

Move _playerButton to the new file and rename it _playPauseButton, to avoid confusion when we are going to add more buttons.

We want to create:

  1. a shuffle button, which will enable or disable shuffle mode, using _audioPlayer.shuffle() to change the order of the audio sources that did not play yet, and _audioPlayer.setShuffleModeEnabled(true) to set the mode of the player to shuffle.
  2. a previous button, that will be enabled if there is a previous audio source, and will load that source when pressed, using _audioPlayer.seekToPrevious();
  3. a next button, that will be enabled if there is a next audio source, and will load that source when pressed, using _audioPlayer.seekToNext();
  4. a loop button, which will cycle among not looping, looping one audio source, and looping the entire sequence of audio sources, using _audioPlayer.setLoopMode(_).

AudioPlayer provides streams for 1 and 4. For 2 and 3 we will need to observe the SequenceState that will emit every time either the sequence of audio sources changes, for instance because the shuffle mode changed, or the current playing audio source changes.

In the build function of PlayerButtons add

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

We added a row, and for each button, we add a stream builder. The first observers the shuffle state of the audio player, the second and fourth observe the sequence state, and the fifth observes the state of the loop mode. The third one is the play button we already build in the previous section.

Now we only need to create four missing methods. The first method is the one that creates a widget for the 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);
      },
    );
}

The second and the third methods create the previous and next buttons.

Widget _previousButton() {
    return IconButton(
      icon: Icon(Icons.skip_previous),
      onPressed: _audioPlayer.hasPrevious ? _audioPlayer.seekToPrevious : null,
    );
}

Widget _nextButton() {
    return IconButton(
      icon: Icon(Icons.skip_next),
      onPressed: _audioPlayer.hasNext ? _audioPlayer.seekToNext : null,
    );
}

Every time the stream _audioPlayer.sequenceStateStream emits a new value, we check whether the current playing source has a previous and a next source in the sequence, and, if so, we set onPressed to the relevant method of _audioPlayer.

The fourth and last method is the one that creates the loop button.

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]);
      },
    );
}

Every time the button is pressed, we cycle through a list of loop modes: off, all, and one.

Adding more buttons to the middle of the screen

Now the app is more interesting. But it is still missing a lot.

In the next article of the series, we will start improving the project repositoryand we will add the list of audio sources, or playlist, that we are playing.

This is the full code for this article.

// main.dart

import 'package:flutter/material.dart';
import 'package:music_player/screens/player.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Player(),
    );
  }
}
// player.dart

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

class Player extends StatefulWidget {
  @override
  _PlayerState createState() => _PlayerState();
}

class _PlayerState extends State<Player> {
  AudioPlayer _audioPlayer;

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

    _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")),
      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")),
      AudioSource.uri(Uri.parse(
          "https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3")),
    ]))
        .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: PlayerButtons(_audioPlayer),
      ),
    );
  }
}
// player_buttons.dart

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

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: [
        StreamBuilder<bool>(
          stream: _audioPlayer.shuffleModeEnabledStream,
          builder: (context, snapshot) {
            return _shuffleButton(context, snapshot.data ?? false);
          },
        ),
        StreamBuilder<SequenceState>(
          stream: _audioPlayer.sequenceStateStream,
          builder: (_, __) {
            return _previousButton();
          },
        ),
        StreamBuilder<PlayerState>(
          stream: _audioPlayer.playerStateStream,
          builder: (_, snapshot) {
            final playerState = snapshot.data;
            return _playPauseButton(playerState);
          },
        ),
        StreamBuilder<SequenceState>(
          stream: _audioPlayer.sequenceStateStream,
          builder: (_, __) {
            return _nextButton();
          },
        ),
        StreamBuilder<LoopMode>(
          stream: _audioPlayer.loopModeStream,
          builder: (context, snapshot) {
            return _repeatButton(context, snapshot.data ?? LoopMode.off);
          },
        ),
      ],
    );
  }

  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),
      );
    }
  }

  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);
      },
    );
  }

  Widget _previousButton() {
    return IconButton(
      icon: Icon(Icons.skip_previous),
      onPressed: _audioPlayer.hasPrevious ? _audioPlayer.seekToPrevious : null,
    );
  }

  Widget _nextButton() {
    return IconButton(
      icon: Icon(Icons.skip_next),
      onPressed: _audioPlayer.hasNext ? _audioPlayer.seekToNext : null,
    );
  }

  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]);
      },
    );
  }
}

The app code is also available on GitHub.

]]>
<![CDATA[ Transform your YouTube subscriptions into an RSS feed ]]> https://ishouldgotosleep.com/transform-your-youtube-subscription-into-an-rss-feed/ 605efc7baba8710001105b71 Programming ]]> Sun, 14 Feb 2021 00:00:00 +0100 One of my new year's resolutions is to change the way I consume information from the Internet. I find myself too many times lost in endless feeds, or ending up on a random video on YouTube after hours of watching other random videos.

I remember once wondering why I was still awake in the middle of the night watching a video of a cat cleaning itself.

In the past it was different. If you needed information you had to search for it. And when (if) you found it, that was it. You either stop there or search for something else. You were not bombarded with a bunch of "related" pages, images, or articles.

But now we check our Facebook timeline to see updates from our friends, and then we are overloaded by posts we do not really care about. We open Instagram, and we see "suggested posts" often between two images uploaded by our contacts.

Why? I am not following thedevlife, why should I see their posts?

We have been silently converted from information seekers to information dumpsters.

Back to RSS

My approach, now, is to get delivered to me only the information I want to have. One way to achieve it is using newsletters. Another one is using RSS readers, an old technology used to gather articles and have them directly delivered to you the moment they are published.

Many services help you with that, but they all need you to create an account, in contrast with another one of my new year resolutions, which consists of reducing the number of digital accounts.

They also store your data for you, which is not how an RSS reader was meant to work. You should be in charge of your data. The reason they store your data is for synchronization purposes. But there are plenty of other synchronization services you already use, so why adding another one, just use one of them.

RSS readers were so popular that even Google maintained one. It was killed in 2013, probably because Google could not monetize on it. Furthermore, Google wants you to search for updates, not just receive them.

I already use RSS readers, luckily a lot of smart people still prefer to blog rather than creating artificially long videos to fill with ads. Hopefully, more people will go back to blogging soon.

I suggest NetNewsWire or An Otter RSS, I like the former more, but it lacks iCloud synchronization. It is an upcoming feature, though. The latter has iCloud synchronization and they are both free.

Get the feed for a YouTube channel

Google used to provide a direct link to the RSS feed of a YouTube channel. They removed the link from the channel page, but they left the feed. You only need to build the link, in this way

https://www.youtube.com/feeds/videos.xml?channel_id=[CHANNEL_ID]

replacing [CHANNEL_ID] with the id of the channel, which you can take from the channel URL.

For instance, the Flutter YouTube channel URL is

https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw

The channel id is, thus, UCwXdFgeE9KYzlDdR7TG9cMw. So the RSS feed URL is

https://www.youtube.com/feeds/videos.xml?channel_id=UCwXdFgeE9KYzlDdR7TG9cMw

Combine all the channel feeds into one

You can directly "subscribe" to each channel's feed, or you can "subscribe" to a big feed that includes all of them, by merging all the feeds together. Go to rssmix.com.

Add all the URLs in the big text area and hit "Create!"

You should save your mix ID so that you can add more channels in the future.

The result

Now you can log out from your YouTube account, only watch videos that are delivered to you via the RSS feed. I also uninstalled the YouTube app from my tablet, I use Safari directly.

]]>
<![CDATA[ Speech wave visualization in SwiftUI ]]> https://ishouldgotosleep.com/speech-wave-visualization-in-swiftui/ 605f162eaba8710001105da6 Tutorials ]]> Thu, 28 Jan 2021 00:00:00 +0100 For a personal project I needed a way to visually represent some speech audio in an iOS app. I decided to go with the old Siri wave animation, something like this.

Fresh from the 100 days of SwftUI animation lessons, I decided to implement it by myself in SwiftUI.

Single wave

The first step is to create a single wave, as a Shape. I took the code from an existing repository linked at the end of the article.

struct Wave: Shape {
    /// The frequency of the sinus wave. The higher the value, the more sinus wave peaks you will have.
    /// Default: 1.5
    var frequency: CGFloat = 1.5

    /// The lines are joined stepwise, the more dense you draw, the more CPU power is used.
    /// Default: 1
    var density: CGFloat = 1.0

    /// The phase shift that will be applied
    var phase: CGFloat
    
    /// The normed ampllitude of this wave, between 0 and 1.
    var normedAmplitude: CGFloat
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let maxAmplitude = rect.height / 2.0
        let mid = rect.width / 2
        
        for x in Swift.stride(from:0, to: rect.width + self.density, by: self.density) {
            // Parabolic scaling
            let scaling = -pow(1 / mid * (x - mid), 2) + 1
            let y = scaling * maxAmplitude * normedAmplitude * sin(CGFloat(2 * Double.pi) * self.frequency * (x / rect.width)  + self.phase) + rect.height / 2
            if x == 0 {
                path.move(to: CGPoint(x:x, y:y))
            } else {
                path.addLine(to: CGPoint(x:x, y:y))
            }
        }
        
        return path
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            Wave(phase: 1.5, normedAmplitude: 0.8)
                .stroke(Color.green)
                .frame(height: 300)
            Spacer()
        }
        .background(Color.black)
        .edgesIgnoringSafeArea(.all)
    }
}

What is happening in the path method is pure math, partially explained here. It is important to note the two main property we are going to change in our animation. The normedAmplitude, which is the amplitude of the audio, normalized between 0 and 1, and the phase, which will shift the wave along the y axis.

The result of the code above is:

Multi wave

Now we want to combine some single waves into a snapshot of the full animation.

struct MultiWave: View {
    var amplitude: CGFloat = 1.0
    var color: Color = Color.green
    var phase: CGFloat = 0.0
    
    var body: some View {
        ZStack {
            ForEach((0...4), id: \.self) { count in
                singleWave(count: count)
            }
        }
    }
    
    func singleWave(count: Int) -> some View {
        let progress = 1.0 - CGFloat(count) / CGFloat(5)
        let normedAmplitude = (1.5 * progress - 0.8) * self.amplitude
        let alphaComponent = min(1.0, (progress/3.0*2.0) + (1.0/3.0))

        return Wave(phase: phase, normedAmplitude: normedAmplitude)
            .stroke(color.opacity(Double(alphaComponent)), lineWidth: 1.5 / CGFloat(count + 1))
    }
    
}

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            MultiWave(amplitude: 0.8, color: .green, phase: 0.0)
                .frame(height: 500)
            Spacer()
        }
        .background(Color.black)
        .edgesIgnoringSafeArea(.all)
    }
}

The code above creates 5 waves, the amplitude of each of them descreasing while also the line width decreases.

It's time to animate

We start adding the animation in the content view:

struct ContentView: View {
    @State private var amplitude: CGFloat = 0.8
    @State private var phase: CGFloat = 0.0
    
    var body: some View {
        VStack {
            Spacer()
            MultiWave(amplitude: amplitude, color: .green, phase: phase)
                .frame(height: 500)
                .onAppear {
                    withAnimation(Animation.linear(duration: 0.1)
                                    .repeatForever(autoreverses: false)
                    ) {
                        self.amplitude = 0.5
                        self.phase -= 1.5
                    }
                }
            Spacer()
        }
        .background(Color.black)
        .edgesIgnoringSafeArea(.all)
    }
}

Bacause Wave is a shape, we need to us an AnimatablePair to get the amplitude and phase to animate:

public var animatableData: AnimatablePair<CGFloat, CGFloat> {
    get {
        AnimatablePair(normedAmplitude, phase)
    }
    
    set {
        self.normedAmplitude = newValue.first
        self.phase = newValue.second
    }
}

Great. It works. Now we want to show it in a prettier way. We need to trigger an animation when a previous animation is completed. We will use some code from Antoine van der Lee and use it in our content view.

struct ContentView: View {
    @State private var amplitude: CGFloat = 0.8
    @State private var phase: CGFloat = 0.0
    @State private var change: CGFloat = 0.1
    
    var body: some View {
        VStack {
            Spacer()
            MultiWave(amplitude: amplitude, color: .green, phase: phase)
                .frame(height: 500)
                .onAppear {
                    withAnimation(Animation.linear(duration: 0.1)
                                    .repeatForever(autoreverses: false)
                    ) {
                        self.amplitude = _nextAmplitude()
                        self.phase -= 1.5
                    }
                }
                .onAnimationCompleted(for: amplitude) {
                    withAnimation(.linear(duration: 0.1)){
                        self.amplitude = _nextAmplitude()
                        self.phase -= 1.5
                    }
                }
            Spacer()
        }
        .background(Color.black)
        .edgesIgnoringSafeArea(.all)
    }
    
    private func _nextAmplitude() -> CGFloat {
        // If the amplitude is too low or too high, cap it and go in the other direction.
        if self.amplitude <= 0.01 {
            self.change = 0.1
            return 0.02
        } else if self.amplitude > 0.9 {
            self.change = -0.1
            return 0.9
        }
        
        // Simply set the amplitude to whatever you need and the view will update itself.
        let newAmplitude = self.amplitude + (self.change * CGFloat.random(in: 0.3...0.8))
        return max(0.01, newAmplitude)
    }
}

We animate the wave to a new "initial" amplitude and phase the wave a bit. Then, when the animation is completed, we get a new, random, amplitude and animate again.

The math is taken from a library that does the same thing with UIKit.

The code is on a repository in GitHub:

mvolpato/SpeechWaveAnimation
A speech wave animation in SwiftUI, based on old style Siri - mvolpato/SpeechWaveAnimation
]]>
<![CDATA[ The basics of security ]]> https://ishouldgotosleep.com/the-basics-of-security/ 605ef246aba8710001105b2f Tue, 12 Jan 2021 00:00:00 +0100 Security is about protecting assets that might be vulnerable to direct and indirect attacks by an intruder, or by system failures and errors.

To effectively protect your assets and improve the security of your apps, and fix security issues, you first need to learn about the basic concepts related to security.

The most general concepts of computer security are confidentiality, integrity, and availability.

ConfidentialityIt ensures that authorized subjects who are not authorized do not have access to the asset. A data breach is a typical confidentiality issue.IntegrityIt ensures that the asset you are protecting, and that holds value for you, cannot be altered by an attacker. Ransomware attacks compromise your data, without the need to access it.AvailabilityIt ensures that the protected asset, that produces value for you, can be accessed by subjects who are authorized to access it. A DDoS attack is a clear attack to availability.

The validity of these concepts in your apps can be supported by useful processes, such as authentication and authorization.

Authentication consists of recognizing the identity of users and the source of assets. It is essential for authorization: to authorize someone we need to know who they are. It is also important that a subject who performed some actions while being authenticated cannot deny performing them at a later time, which is called non-repudiation.

Even authorized actions, performed by subjects who are apparently authenticated, could be security violations, because there might be vulnerabilities in authentication controls. For this reason, all authentication processes should be logged by auditing.

Auditing requires

  • selection of dangerous actions to control;
  • protection of log files;
  • authentication, again, to link dangerous actions to the subject who performed them.

Security policies

So, what do you need to do to ensure your app is secure? Well, you cannot be 100% sure that your app has no vulnerabilities. Just by installing it on an operating system, you inherit the vulnerabilities exposed in that operating system.

But you can minimize the number of security issues by following security policies.

Security policies are defined by you and your team, and their job is to remind you of possible vulnerabilities by providing you a way to avoid them.

They define rules regarding:

  • actions that can be performed on some assets, and the users who are allowed to perform those actions;
  • which users can access which data;
  • which users have special privileges, and what those privileges allow them to do.

To defined security policies we need to

  • determine what assets we want to protect;
  • learn how the asset works and interacts with other things;
  • determine how our asset's value is reduced directly or indirectly.

The value of the asset is also important in defining how to protect it. You do not want to spend $10k on security measures to protect an asset with a value of $10.

Security principles

Security principles aid in selecting and designing the correct mechanisms to implement your goals.

Keep it simple

Do not overcomplicate your app design. Each level of complication will add multiple levels of security mechanisms needed to avoid vulnerabilities.

If you add (many) libraries to your app, you need to:

  • regularly scan for known vulnerabilities in the library code, if you have access to it, to avoid vulnerabilities in the library;
  • test (or trust) the vendor code, if you do not have access to it;
  • apply your policies when integrating the library in your app.

If you use containers you need to know how to properly secure them. Many container's images are meant for development and thus are not secure for production. Remember that many tutorials online leave the system with vulnerabilities, because the author wants to make the tutorial simple.

If your app architecture is hard to decouple, it will be difficult to identify security vulnerabilities. If, on the other hand, it is simple, you can concentrate on one small part at a time to identify, control, and fix them.

Safe default

If the system fails, make sure that it will default to a safe state. Do not log errors that could give information an attacker could use.

If an admin login request fails, do not assume the requester is a normal user, check their authenticated identity again, against a non-admin account.

If your app fails to retrieve payment data, do not default to the paid version, but show an error interface.

Always authenticate

Each action in your app against an API should be verified for authentication and authorization. Do not assume that the user is always authenticated after login.

For instance, let's say that I let users download some books from my server after they logged in successfully in my app, but without authenticating them again on the download request. Then an attacker can recognize that request by just investigating the network traffic and download the books by repeating that request, from outside the app, without the need to be authenticated.

Do not rely on obscurity

Do not "secure" your app by using unknown algorithms to hide your data, and do not just obscure the algorithms that you use to encrypt your data. Once your app is installed on a user device. the code can be extracted and studied.

Separate privileges

Use different levels of users for different tasks. This is also important in your team, not only in your app.

Karen from marketing does not need to have access to the source code.

Least privilege

When you perform a task, you should use an account that has the minimum necessary privileges to perform that task. Do not use a single admin account all the time.

If that account is compromised, all the data it has access to is compromised.

Security and psychology

Security is rarely appreciated by the users. The majority of users know nothing about security, still, they are those who need it the most. There are conflictsbetween security and the simplicity of usage of software.

Adding security will increase computing usage, change the routine for existing users, and maintaining security will cost more.

But you should not make security difficult for your users. Otherwise, they will find a way to work around it.

For instance, if you require users to change their password every month, they will start using the same password every month, only changing the final character: Password1, Password2, ... PassowordN.

Inconvenient attack

If possible, design your security such that the tools an attacker needs to break it are more valuable than the asset. For instance, if an attacker needs to run expensive gear for days to break your security measures and access an asset with a value of $1k, then it might be more profitable for them to mine Bitcoins instead.

Auditing

Log all authentication requests and all actions that need an authenticated user to be performed, for two main reasons:

  1. Given an action, for instance, the deletion of a file, you can always trace it back to the user who performed it;
  2. If an account is compromised, you can trace all the actions taken by that account to verify what data has been possibly exposed.

Defense in depth

Design your security mechanisms so that multiple, redundant measures are in place.
This way, if one of them fails, there will be other mechanisms still in place to protect your assets.

Single point of failure

Avoid placing all your assets in the same place. A single attack can compromise all of them at the same time. For instance, if you have a single DB with all the information of your app, then once that is compromised, all data is compromised.

]]>
<![CDATA[ How to backup Apple Notes on macOS, using Automator ]]> https://ishouldgotosleep.com/how-to-backup-apple-notes-from-your-mac/ 605efacbaba8710001105b58 Programming ]]> Sun, 03 Jan 2021 00:00:00 +0100 Apple Notes are an easy way to keep your thoughts organized and synced among all your Apple devices. And it is free. Notes can be synced via iCloud, or, if you are concerned about privacy, kept On My Mac only.

The Apple Notes app is my favorite note-taking app because it is fast, easy to use, and easy to sync.

If, like me, you use Apple Notes to organize your work and your life, you want to be sure that you do not risk to lose them accidentally, either because of your mistake or because of a bug.

Unfortunately, there is no backup function in Apple Notes. You can copy the notes database (try to look in the folder ~/Library/Group Containers/group.com.apple.notes/), but if you want to backup a readable format of your notes, you must export each single note as PDF from the File menu, and when you have hundreds of notes, that quickly becomes annoying.

Automator to the rescue

Given that Notes is scriptable -to learn what you can do via script, open Script Editor on macOS and File - Open dictionary... and then choose Notes.app- which means that you can use Automator to export every note one by one.

In fact, you can just copy this code and past it in a Run AppleScript step of a new workflow in Automator:

set exportFolder to (choose folder) as string

-- Simple text replacing
on replaceText(find, replace, subject)
    set prevTIDs to text item delimiters of AppleScript
    set text item delimiters of AppleScript to find
    set subject to text items of subject
    
    set text item delimiters of AppleScript to replace
    set subject to "" & subject
    set text item delimiters of AppleScript to prevTIDs
    
    return subject
end replaceText

-- Get an HTML file to save the note in.  We have to escape
-- the colons or AppleScript gets upset.
on noteNameToFilePath(noteName)
    global exportFolder
    set strLength to the length of noteName
    
    if strLength > 250 then
        set noteName to text 1 thru 250 of noteName
    end if
    
    set fileName to (exportFolder & replaceText(":", "_", noteName) & ".html")
    return fileName
end noteNameToFilePath

tell application "Notes"
    
    repeat with theNote in notes of default account
        
        set noteLocked to password protected of theNote as boolean
        set modDate to modification date of theNote as date
        set creDate to creation date of theNote as date
        
        set noteID to id of theNote as string
        set oldDelimiters to AppleScript's text item delimiters
        set AppleScript's text item delimiters to "/"
        set theArray to every text item of noteID
        set AppleScript's text item delimiters to oldDelimiters
        
        if length of theArray > 4 then
            
            -- the last part of the string should contain the ID
            -- e.g. x-coredata://39376962-AA58-4676-9F0E-6376C665FDB6/ICNote/p599
            set noteID to item 5 of theArray
        else
            set noteID to ""
        end if
        
        if not noteLocked then
            
            set fileName to ("[" & noteID & "] " & (name of theNote as string)) as string
            set filepath to noteNameToFilePath(fileName) of me
            set noteFile to open for access filepath with write permission
            set theText to body of theNote as string
            write theText to noteFile as Unicode text
            
            close access noteFile
            
            tell application "Finder"
                
                set modification date of file (filepath) to modDate
            end tell
        end if
        
    end repeat
    
end tell

Then start the workflow. It will ask for the folder where to save your notes, and then it will (slowly) create an HTML file for each note in your app.

I am not the creator of this script, but I cannot find the original source anymore ๐Ÿคทโ€โ™‚๏ธ.

]]>
<![CDATA[ Introduction to (cyber)security ]]> https://ishouldgotosleep.com/introduction-security-mobile-development/ 605ef153aba8710001105b11 Fri, 25 Dec 2020 00:00:00 +0100 In the last years, cyber attacks have been on the rise. In 2018 there were 11% more security breaches compared to 2017, and 67% more compared to 2013.

A cyber attack can result in a data breach, a ransom request, or it can compromise the same tools that are supposed to protect you.

A security flaw can also be inherited by a library that you use.

AppSec

We rely on our mobile devices every day, but they can also be subject to vulnerabilities. In fact, ~91% of iOS apps and ~95% of Android apps analyzed in this report contain flaws.

Such apps could be those that we use for accessing our bank account, controlling our home internet connected accessories, handling our personal pictures and texts.

It is up to us, programmers, to do our best to avoid these problems for our users. Thus, when we create and maintain mobile applications, it is important to include security from the very beginning, not only in the development process, during requirements definitions, architecture design, development, and testing, but also in everyday business-related processes. We need to make sure that our everyday work has security integrated into it.

We can identify two main classes of potential pitfalls:

  • User authentication relates to the usage of the app by an attacker who is posing as another user. It is mitigated by the login system of the device in use: if the access to the smartphone is secured by a pin, or by biometric authentication, such as Face ID, then the app might not need another authentication level. But if the app handles particularly private data, like banking information, then it should implement an additional authentication step. On the other hand, most apps connect to a backend system, against which the user needs to login again. In this case, the device login system is not enough.
  • Data access relates to the communication between the app and services exposed on the internet. We cannot assume that such communications are performed on private networks, so the data being transmitted can be intercepted, read, and potentially modified by an attacker.

User authentication

It is fundamental that users are correctly identified and that the actions they perform are allowed concerning their identity. That means that a user, when logged in, is correctly identified by the system, but she is not able to read personal information about other users.

The availability of service over the internet makes it more complicated. When a user needs to authenticate to a remote service, passwords cannot be transmitted as clear text.

Data access

All data must be encrypted so that it is incomprehensible for an attacker without the proper decryption key.
Encryption is important, but it does not solve all security problems: if an attacker manages to get identified as an admin, cryptography won't matter anymore.

Other cyberattacks

There are also other cyberattacks we need to consider when designing security in our apps:

  • viruses that a user executes without realizing it;
  • denials of service, where an online service is taken down by an excessive number of requests;
  • abuses of service, where an attacker finds a way to abuse a service you provide online;
  • pishing scams, and other forms of social engineering attacks; and others.

When designing and implementing an app, we might need to consider those as well.

We also need to keep in mind that designing complex security mechanisms will make it easier to oversee bugs, opening possible vulnerabilities for attackers.

Conclusions

There are many types of vulnerabilities we need to take into account when designing a new app. We need to keep our strategy simple, first by understanding the basic components of cybersecurity and how cryptography works, then by exploring the most common vulnerabilities in apps and understanding how to avoid or fix them, and then by defining security-related processes that we can apply during the entire software development lifecycle.

]]>
<![CDATA[ Firebase to Google Sheets, a tech security clarification ]]> https://ishouldgotosleep.com/firebase-to-google-sheets-a-tech-security-clarification/ 605f15b7aba8710001105d97 Tutorials ]]> Wed, 09 Dec 2020 00:00:00 +0100 In a previous article, I show how to synchronize data from Firestore to a spreadsheet in Google Drive using a Cloud Function triggered by the creation of a new document in Firestore.

The approach used in that article, namely, deploying a service account with access to the Google Drive API with the Cloud Functions using it, is not safe.

Here I propose two solutions, both compatible with the rest of the article, that can replace the deployment of the service account with the Cloud Functions.

Add Google Drive API permission to the default service account

When Cloud Functions run, some default authorization credentials are automatically populated. The default service account is in the form PROJECT_ID@appspot.gserviceaccount.com, in my case selling-soul@appspot.gserviceaccount.com, and it is named App Engine default service account.

After enabling access to Google Sheets API from the previous article, instead of creating a new service account, you can use the default one.

We need to share the spreadsheet with PROJECT_ID@appspot.gserviceaccount.com, then we use the Application Default Credentials to authorize our request in src/exportBids.ts:

import * as functions from 'firebase-functions'

// Google Sheet
import { google } from 'googleapis'
const sheets = google.sheets('v4')

const authClient = google.auth.getClient({
    scopes: ['https://www.googleapis.com/auth/spreadsheets']
});

export async function exportBids(
    id: string,
    username: string,
    date: string,
    bids: Array<any>
) {

    console.info(`Exporting bids ${id}`)

    const finalData: Array<Array<string>> = []
    bids.forEach(function (bid) {
        finalData.push([id, date, username, bid.bidder, bid.offer])
    })

    await sheets.spreadsheets.values.append({
        auth: await authClient,
        spreadsheetId: "1YpO8oe2I8cEImtKjnBscOLbiNnnTaDP1e82YAaymu20",
        range: `Sheet1!A1:E1`,
        valueInputOption: 'RAW',
        requestBody: { values: finalData, majorDimension: "ROWS" }
    }, {})
}

Using Google Secret Manager

The other solution is to use Google Secret Manager, which we will explain in detail in a future article.

]]>
<![CDATA[ Fix the error ITMS 90208: Invalid Bundle. When uploading your app on Apple Store Connect ]]> https://ishouldgotosleep.com/error-itms-90208-invalid-bundle-the-bundle-app-framework-does-not-support-minimum-os-version-specified-info-plist/ 605f1085aba8710001105cd3 Tips ]]> Wed, 25 Nov 2020 00:00:00 +0100 Today I had to update a Flutter app that I successfully uploaded on the App Store Connect some months ago.

In the meanwhile Flutter and the project dependencies were updated, and the app was working both in debug mode and when distributed in-house.

While uploading I received this error:

ERROR ITMS-90208: "Invalid Bundle. The bundle Runner.app/Frameworks/App.framework does not support the minimum OS Version specified in the Info.plist."

The quick workaround I tried was to change the MinimumOSVersion in ios/Flutter/AppFrameworkInfo.plist from 8.0 to 9.0:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>$(DEVELOPMENT_LANGUAGE)</string>
        <key>CFBundleExecutable</key>
        <string>App</string>
        <key>CFBundleIdentifier</key>
        <string>io.flutter.flutter.app</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>App</string>
        <key>CFBundlePackageType</key>
        <string>FMWK</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
        <string>1.0</string>
        <key>MinimumOSVersion</key>
        <string>9.0</string>
    </dict>
</plist>
]]>
<![CDATA[ How to read free Medium articles without an account ]]> https://ishouldgotosleep.com/how-to-read-medium-articles-without-an-account/ 605ef9d0aba8710001105b42 Programming ]]> Mon, 23 Nov 2020 00:00:00 +0100 Medium is a platform that allows you to earn money from your writing.

Although I do understand that writers want to get recognized and give the possibility to their readers to support their work, I am convinced that, as a service, Medium is unnecessary.

Using Medium for articles about programming

It does not encourage quality, especially in the programming community.

Authors have an especially horrible code block at their disposal, without the possibility to style it, and they end up hosting the code somewhere else, using GitHub Gists, which can stop working (and did stop working for a while), at any moment, or, even worse, they end up using images ๐Ÿคฎ.

If you are part of the programming community, and you use Medium because you do not know how to set up a personal blog for free, then whatever you have to write loses credibility.

Furthermore, the fact that it is so easy to put a paywall in front of your content, can convince you that you should enable such a paywall, regardless of the quality or regardless of the goal your content has.

Each article should have a specific purpose: some of them are meant to lead organic traffic to you, some others are meant to establish your authority, and some others are meant to convert readers to customers, whatever customer means to your specific case.

You definitely do not want to hide behind a paywall the first type of article. But it is too easy to forget that and hide all your articles, instead.

And I am not the only one thinking it in this way.

Accounts overload

So now I am in this situation where there might be a cool article on Medium I might want to read, but I already read three low-quality articles today, reaching the quota for free articles if you do not have an account.

In today's age of accounts overload, companies make it easy for us to just create another free account. Giving away your email address and other personal information to more and more companies, and to other companies with whom they work.

Furthermore, having more accounts means having a higher probability of getting your information into the hands of hackers and spammers.

For these reasons, I try to keep new registrations with my personal information to a minimum.

Medium own thirst for new users backfires

How can we, then, read one more article without an account?

Medium needs to allow search engines to access the entire article, without accounts, because search engines only use bots for scraping new content. Medium needs it to get articles in the result pages of search engines.

And we are going to exploit exactly this.

On Google, on the search result page, we can click on the small triangle next to the title of the article we want to read, in order to access the cached version:

and then click on Text-only version:

]]>
<![CDATA[ Connect the Firebase authentication emulator from your Flutter app ]]> https://ishouldgotosleep.com/connect-firebase-authentication-emulator-flutter-app/ 605f1045aba8710001105cc2 Flutter, Tips ]]> Sun, 08 Nov 2020 00:00:00 +0100 Firebase relased the Authentication emulator recently.

Firebase project

You can use it in your existing Flutter app by adding, in firebase.json:

"auth": {
    "port": "9099",
    "host":" "0.0.0.0"
}

to the "emulators" object in your Firebase project. You can change port and host according to your needs.

Then you can run

firebase emulators:start --only functions,firestore,auth

replacing the list of emulators with your own list.

Flutter project

The FireFlutter plugin still does not allow to set the emulator for Firebase Authentication. You can set it up manually for iOS and Android.

For instance, for iOS, you can open ios/Tunner/AppDelegate.swift and add this line to the didFinishLaunchingWithOptions method:

Auth.auth().useEmulator(withHost: '0.0.0.0', post: 9099)

Also, remember to import Firebase in AppDelegate.swift:

import Firebase
]]>
<![CDATA[ Connect Firebase Analytics and BigQuery with Google DataStudio using custom queries ]]> https://ishouldgotosleep.com/connect-firebase-analytics-bigquery-datastudio-custom-query/ 605f14f9aba8710001105d7a Tutorials ]]> Sun, 18 Oct 2020 00:00:00 +0200 Firebase Analytics is a powerful tool to get insights into how the users of your app behave.

You can see the trend of the number of active users, daily user engagement, and if you have custom numeric parameters associated with your events, you can even see graphs with the average or sum of the value of that parameter.

But the basic reporting capability of Firebase Analytics is very limited. You cannot perform custom queries of any form, so you are bounded to the default graphs and tables.

For instance, let's say your app allows users to send stickers. The app tracks the shape and color of the stickers, together with the number of stickers, by using custom-parameter reporting.

In Firebase Analytics you can see the total number of stickers sent, and, with filtering, you can also see the total sum for each color or each shape.

But it is not possible to have, in a single table, the total number of sent stickers grouped by color and by shape.

BigQuery to the rescue

To be able to get the best out of your Firebase data, you need to connect analytics to BigQuery, a cost-effective big data tool from Google.

Linking your Firebase Analytics to BigQuery can be done from the Firebase project settings, in the Integrations.

Search for BigQuery, and click Link, Next, then enable Google Analytics and click Link to BigQuery.

Now your analytics data will be loaded in BigQuery, but it can take up to 24 hoursto see it.

Connect BigQuery to Data Studio

You do not need to use BigQuery directly, you can use other tools to visualize the data in it. For instance, Google Data Studio. Start a new Blank Report

In the Add data to report Connect to data tab select BigQuery. Select Custom queryand then select the correct project.

In the Enter Custom Query field enter your query, for the data above this will do:

SELECT SUM(send_amount) AS total, color, shape
FROM ( 
    SELECT event_name, event_timestamp, user_pseudo_id, 
        (SELECT value.int_value FROM UNNEST(event_params) 
            WHERE key = "send_amount") AS send_amount,
        (SELECT value.string_value FROM UNNEST(event_params) 
            WHERE key = "color") AS color,
        (SELECT value.string_value FROM UNNEST(event_params) 
            WHERE key = "shape") AS shape
    FROM `PROJECT_ID.analytics_TABLE_NUMBER.events_*`
    WHERE event_name = "send"
    AND _TABLE_SUFFIX BETWEEN @DS_START_DATE AND @DS_END_DATE
)
GROUP BY color, shape

where PROJECT_ID is your project id from Firebase, and the TABLE_NUMBER can be found from your BigQuery console.

Enable Enable date parameters, which allows using @DS_START_DATE and @DS_END_DATE in the query above, so that the viewer of the report can select a time period.

If you are curious about the UNNEST command, you can read more about it in the articles from the Firebase team How to use the UNNEST function in BigQuery to analyze event parameters in Analytics and How to use SELECT FROM UNNEST to analyze multiple parameters in BigQuery for Analytics.

Show the data in Data Studio

In editor mode in Data Studio select Add a chart and click on Table. In Dimensionselect Color and Shape. In Metric select total and as its aggregation select SUM.

Now your table should show the total sum aggregated by two custom parameter values.

]]>
<![CDATA[ Cloud Functions stopped having access to Firestore ]]> https://ishouldgotosleep.com/cloud-functions-stopped-having-access-to-firestore/ 605f0fe0aba8710001105cb5 Tips ]]> Tue, 13 Oct 2020 00:00:00 +0200 After adding automatic backup to a Firebase project I am working on, Cloud Functions started getting an error when accessing Firestore:

Error code 7: 'Missing or insufficient permissions.'

The solution was to add the Editor role to the default service account.

]]>
<![CDATA[ Remove nil elements from an array while changing the array type ]]> https://ishouldgotosleep.com/functionally-remove-nil-elements-from-an-array-while-changing-the-array-type/ 605f0fafaba8710001105caa Tips ]]> Mon, 12 Oct 2020 00:00:00 +0200 I always forget how to remove nil elements from an array in a functional way in Swift.

Here it is:

let arrayWthOptionals: [String?] = ...
let arrayWithoutOptionals: [String] = arrayWthOptionals.compactMap ({ $0 })

The resulting array is an array of non-optional.

]]>
<![CDATA[ Fix localhost invalid domain in Flutter 1.22 ]]> https://ishouldgotosleep.com/fix_localhost_invalid_domain_in_flutter/ 605f0f59aba8710001105c96 Flutter, Tips ]]> Sun, 04 Oct 2020 00:00:00 +0200 Recently, I update Flutter to version 1.22 and one of my apps stopped working on iOS, giving the following error:

Runner[52852:484760] [VERBOSE-2:shell.cc(210)] Dart Unhandled Exception: Invalid argument (domain): Invalid domain name: "localhost", stack trace: #0      new _DomainNetworkPolicy (dart:io/network_policy.dart:85:7)
#1      _constructDomainPolicies (dart:io/network_policy.dart:181:22)
#2      _EmbedderConfig._setDomainPolicies (dart:io/embedder_config.dart:44:23)

It looks like a change in the network policy does not allow to defined localhost as a valid domain.

I am currently using localhost to connect to a local Cloud Functions emulator, by instantiating Cloud Functions with

CloudFunctions(region: region)
    .useFunctionsEmulator(origin: "http://localhost:5001")

and allowing http connection in the Info.plist:

...
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
...

Apparently this was broken with Flutter 1.22.

My solution was to remove the exception from the Info.plist file, by removing the lines above, and then adding a host to the firebase.json of the local Firebase project, where the emulators are defined:

...
"emulators": {
    "functions": {
        "port": 5001,
        "host": "0.0.0.0" <-- added this
    },
...

This led to an update of instantiation of Cloud Functions in code:

CloudFunctions(region: region)
    .useFunctionsEmulator(origin: ""http://0.0.0.0:5001")

and everything started working again.

There is another change in Flutter 1.23 that might break this again. I will update this short article if needed.

]]>
<![CDATA[ How to fix 'To use the 'java' command-line tool you need to install a JDK' error ]]> https://ishouldgotosleep.com/fix-you-need-to-install-a-jdk-error/ 605f0ef6aba8710001105c85 Tips ]]> Fri, 02 Oct 2020 00:00:00 +0200 You can solve the error "To use the โ€œjavaโ€ command-line tool you need to install a JDK. Click โ€œMore Infoโ€ฆโ€ to visit the Java Developer Kit download website." on macOS, by installing a version of the JDK.

You do not need to install the official Oracle ones, you can install the AdoptOpenJDK ones by running

brew cask install adoptopenjdk

This will save you a preference panel in System Preference, and you can manage the installation with brew cask.

]]>
<![CDATA[ You do not need permission to take photos in Android ]]> https://ishouldgotosleep.com/you-do-not-need-permission-to-access-camera-in-android/ 605f0e91aba8710001105c7b Tips ]]> Wed, 30 Sep 2020 00:00:00 +0200 The android.media.action.IMAGE_CAPTURE intent does not need the Camerapermission. By using it, your app asks the operating system to open the camera app and expects to receive a photo from it.

Your app is not accessing the camera hardware directly, so it does not need the Camera permission.

But, if you are using it in a Flutter app that uses a plugin that declares Camerapermission, then you might first need to request permission to the user, before using the camera.

]]>
<![CDATA[ Discriminate Flutter flavors that should build with signing ]]> https://ishouldgotosleep.com/discriminate-flutter-flavors-that-should-build-with-signing/ 605f0e4faba8710001105c6d Flutter, Tips ]]> Fri, 25 Sep 2020 00:00:00 +0200 I am working on a Flutter app with different flavors. There are currently three flavors configured in the app:

  • development connects to local Firebase emulators, it is not signed, it's used only to run on simulators or devices.
  • uat connects to a staging Firebase project, it is not signed for App Store or Play Store distribution, it is distributed via Firebase App Distribution.
  • release connects to a production Firebase project, it is signed for store distribution.

How we implemented flavors is very similar to the guides linked in Flutter documentation.

After having development and uat working, we added signing for release, following the official guide, and at that point, Android uat stopped working. On building time we experienced the following error.

...
* What went wrong:

Execution failed for task ':app:validateSigningUatRelease'.

> Keystore file not set for signing config release

The interesting part of android/app/build.gradle is:

signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
        storePassword keystoreProperties['storePassword']
    }
}

buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}

flavorDimensions "flavors"
productFlavors {
    dev {
        dimension "flavors"
        applicationIdSuffix ".dev"
        versionNameSuffix "-dev"
    }
    uat {
        dimension "flavors"
        applicationIdSuffix ".uat"
        versionNameSuffix "-uat"
    }
    release {
        dimension "flavors"
        // This is the play store release version.
        applicationId "com.ishouldgotosleep.android"
    }
}
}   

The problem is with the release inside buildTypes. It does not identify the releaseflavor, but the Flutter release.

To fix it we changed it to

signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
        storePassword keystoreProperties['storePassword']
    }
}

flavorDimensions "flavors"
productFlavors {
    dev {
        dimension "flavors"
        applicationIdSuffix ".dev"
        versionNameSuffix "-dev"
    }
    uat {
        dimension "flavors"
        applicationIdSuffix ".uat"
        versionNameSuffix "-uat"

        // Set the signing configuration. 
        signingConfig signingConfigs.debug
    }
    release {
        dimension "flavors"
        // This is the play store release version.
        applicationId "com.ishouldgotosleep.android"

        // Set the signing configuration. 
        signingConfig signingConfigs.release
    }
}

We removed the buildTypes section and added the signing information directly in the productFlavors section.

]]>
<![CDATA[ Update Google Sheets from Firestore using Cloud Functions ]]> https://ishouldgotosleep.com/update-google-sheets-from-firestore-trigger/ 605f1343aba8710001105d41 Tutorials ]]> Wed, 23 Sep 2020 00:00:00 +0200 Updated on Dec 8, 2020

It is not safe to deploy a service account json file with your Cloud Functions

follow-up article

If you do not measure the data that is produced by your app, you cannot know how to improve it the right way. The consumer of such data has usually a preferred tool to analyze it. It could be a set of Python scripts, it could be an external tool like Tableau, or it could be a simple spreadsheet software, like Google Sheets.

The data analyst is going to come to you and ask you to export some data from your app to a spreadsheet on Google Drive. You can use third-party services, like Zapier. But you are a passionate programmer, and you want to save your company some money. So you decide to export the data yourself, in an automated way.

In this tutorial, you will learn how to automatically append a new row in a Google Sheets file whenever a new document is created in Firebase Firestore.

Setup the project

If you do not already have a Firebase project, create a new one, go to the Firebase console and click Add project.

Give a name to your project, I chose "Selling soul", and click Continue.

Whether to enable or disable Google Analytics is up to you, click Create project, and wait for the creation process to finish. Then select Cloud Firestore and click Create database.

Select Start in production mode and click Next, select your preferred location, I selected eur3, and click Enable.

Enable Google Sheets API

Now that you are ready with your Firebase project, go to the Google Cloud Consoleand make sure to select the correct project.

Search in the search bar for "google sheets api" and click Enable. Once enabled click Credentials

Click + CREATE CREDENTIALS, select Service account, and, on the new page, give a name to the service account, for instance Google sheets updater, add a description if you like, and click CREATE. You can leave the optional permissions empty and click CONTINUE. You can leave the optional grant user access empty as well, and click DONE.

Now the new service account will appear in the list of service accounts.

Click it and write down its email address, in my case google-sheets-updater@selling-soul.iam.gserviceaccount.com, then click ADD KEY, selecting Create new key. Choose JSON. After clicking CREATE, a json file containing the key that allows you to use this service account.

Do not keep this file in version control, because it contains a private key. If you lose this file, you will need to create a new key.

Share the spreadsheet

If you do not have one already, create a new Google Sheets in Google Drive, and share it with the email address of the service account.

Write down the file id of the spreadsheet. You can get the id from the URL, for instance the spreadsheet at the URL https://docs.google.com/spreadsheets/d/1YpO8oe2I8cEImtKjnBscOLbiNnnTaDP1e82YAaymu20has id 1YpO8oe2I8cEImtKjnBscOLbiNnnTaDP1e82YAaymu20.

Create a cloud function

Follow the official get started guide to install firebase-tools, then log in and start using Cloud Functions.

firebase login
firebase init functions

Select Use an existing project and select your Firebase project. Select Typescript, answer n to Do you want to use ESLint to catch probable bugs and enforce style?and y to Do you want to install dependencies with npm now?.

The init process should have created a functions folder, move the service account JSON file in it, but, remember, do not version control it. I would add it to .gitignore. Rename the file to sheets_updater_service_account.json.

It is not safe to deploy a service account json file with your Cloud Functions

follow-up article

In terminal move to the functions folder and install some needed packages.

cd functions
npm install firebase-functions googleapis

Open src/index.ts, it's time to write code. Replace the content with

import * as functions from 'firebase-functions'
import { exportBids } from './exportBids'
const region = "europe-west3"

// Triggered when a group of bids is added to the bids collection.
// This will update a spreadsheet with history data in Google Drive.
exports.exportOrderData = functions.region(region).firestore
    .document('bids/{id}')
    .onCreate(async (snapshot, _) => {

        const id = snapshot.id

        const documentData = snapshot.data()
        const username = documentData.username
        const date = documentData.date.toDate().toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'long' })
        const bids = documentData.bids // Array of bids

        await exportBids(id, username, date, bids)
});

and create a new file src/exportBids.ts with the following content, replacing the spreadsheet id with your own.

import * as functions from 'firebase-functions'

// Google Sheet
import { google } from 'googleapis'
const sheets = google.sheets('v4')

const serviceAccount = require('../sheets_updater_service_account.json')

const jwtClient = new google.auth.JWT({
    email: serviceAccount.client_email,
    key: serviceAccount.private_key,
    scopes: ['https://www.googleapis.com/auth/spreadsheets']
})

const jwtAuthPromise = jwtClient.authorize()

export async function exportBids(
    id: string,
    username: string,
    date: string,
    bids: Array<any>
) {

    console.info(`Exporting bids ${id}`)

    const finalData: Array<Array<string>> = []
    bids.forEach(function (bid) {
        finalData.push([id, date, username, bid.bidder, bid.offer])
    })

    await jwtAuthPromise
    await sheets.spreadsheets.values.append({
        auth: jwtClient,
        spreadsheetId: "1YpO8oe2I8cEImtKjnBscOLbiNnnTaDP1e82YAaymu20",
        range: `Sheet1!A1:E1`,
        valueInputOption: 'RAW',
        requestBody: { values: finalData, majorDimension: "ROWS" }
    }, {})

}

Build the functions and deploy them with

npm run build
firebase deploy --only functions

Test the result

Go to the web interface of Cloud Firestore for your project and add a new collectioncalled bids. You will need to add a document as well in the collection.

Check the spreadsheet, there should be some new rows, one for each element of the array bids.

]]>
<![CDATA[ Long text is not displayed correctly when using ellipsis overflow in Flutter ]]> https://ishouldgotosleep.com/long_text_is_not_displayed_correctly_when_using_ellipsis_overflow_in_flutter/ 605f0e01aba8710001105c5e Flutter, Tips ]]> Fri, 18 Sep 2020 00:00:00 +0200 I was working with a list of single line Text widgets in Flutter, and I noticed that the text was not displayed properly.

For instance

Text(
    "A very long text_that_is_too_long_for_the_width_of_this_widget",
    maxLines: 1,
    overflow: TextOverflow.ellipsis,
    softWrap: false,
)

resulted in something like

A very long...

when the expected result was something like

A very long text_that_is_too_long_for...

There seems to be a bug in Flutter, opened in June 2018, that hopefully will be fixed soon.

As suggested by a contributor I decided to use TextOverflow.fade, instead of using workarounds that will only make the code more complicated, compared to a small change in the design.

]]>
<![CDATA[ Using git rebase to squash commits ]]> https://ishouldgotosleep.com/using-git-rebase-to-squash-commits/ 605f0d9baba8710001105c4a Tips ]]> Fri, 11 Sep 2020 00:00:00 +0200 On the same note of yesterday's article, today I worked on a very small change, but I had to comment some code to be able to test locally, without the need to connect to the real backend.

I made the changes and committed, and then I realized that the code was still commented. So I uncommented that code and committed again.

Now the history of the repository is not clean, there is a uncomment code for local dev commit that I did not like.

The solution is to squash the latest commit into the previous one:

git rebase -i HEAD~2

which opens an editor

pick 127db81 Add app version to login screen
pick b9e4be5 Uncomment code for local dev

# Rebase b3fe2b2..b9e4be5 onto b3fe2b2 (2 commands)
#
# Commands:
# p, pick  = use commit
# r, reword  = use commit, but edit the commit message
# e, edit  = use commit, but stop for amending
# s, squash  = use commit, but meld into previous commit
# f, fixup  = like "squash", but discard this commit's log message
# x, exec  = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop  = remove commit
# l, label  = label current HEAD with a name
# t, reset  = reset HEAD to a label
# m, merge [-C  | -c ]  [# ]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c  to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
Terminal

I wanted to squash b9e4be5 into 127db81, but I did not care about the commit message, so I changed the first lines to

pick 127db81 Add app version to login screen
f 9cd817a Uncomment code for local dev

where f stands for fixup which discard the commit's log message. When I closed the editor, I was greeted by

Successfully rebased and updated refs/heads/feature/add-version-login.

and I was ready to push to remote.

]]>
<![CDATA[ Consider git rebase instead of git merge ]]> https://ishouldgotosleep.com/consider-git-rebase-instead-of-git-merge/ 605f0d62aba8710001105c3c Tips ]]> Thu, 10 Sep 2020 00:00:00 +0200 You are working on a branch feature/new-feature to add a new feature. You are ready to push to remote and start a pull (or merge) request.

Some of your colleagues already pushed and merged new code in the remote repository, so you need to pull those changes first and merge them with your local copy.

git fetch
git merge master

and you are ready to push. The last commit in your history will be a merge commit.

But if you worked on feature/new-feature for a small amount of time, and you did not push any change already, so that nobody could have pulled it locally, you might want to rebase instead of merge.

You can do it with

git rebase master

The difference is that now, from your repository history, it looks like you started working on feature/new-feature from the new master.

Another difference is that you will need to solve conflicts by one by one and not all together.

First find the conflicts with git diff, then fix it. Then tell git that you solved it with git add <filename> and continue with the rebase git rebase --continue.

]]>
<![CDATA[ Background HealthKit observer queries between app launches ]]> https://ishouldgotosleep.com/background-healthkit-observer-queries-between-app-launches/ 605f0d24aba8710001105c30 Tips ]]> Mon, 07 Sep 2020 00:00:00 +0200 Today I needed to debug some code that is using HealthKit, running observer queries in the background, also when the app is not running.

The app could create multiple observer queries on the same sample types, but only the one created last should be considered the correct one.

There is no much information in the documentation. For instance:

  • Does the execution of a new observer query stop other queries on the same data type started earlier?
  • If you start an observer query running in the background, then kill your app and launch the app again, is the query still running?

After some empirical tests, these are my conclusions:

  • You can execute multiple observer queries on the same sample types in your app. They will all execute independently when new samples are added to HealthKit.
  • On the next app launch, they will be stopped and you need to start a new observer query.

I still need to test what happens when your users update the app. Is the query stopped? Does the user need to open the app after update to get a new query to be executed?


Please let me know if you have evidence that disproves my theory above. I would be happy to be enlightened on this subject.

]]>
<![CDATA[ Create password protected archives from the terminal ]]> https://ishouldgotosleep.com/create-password-protected-archives-from-terminal/ 605f0b4aaba8710001105c24 Tips ]]> Sat, 05 Sep 2020 00:00:00 +0200 You can easily protect archives created in the terminal in macOS. If you want to create a zip file of the folder archiveMe you can run the command

zip -er Archive.zip archiveMe

where the option e tells the command to encrypt the zip file, and r tells the command to recurse into directories.

The command will wait for you to input the password (twice) before creating the archive.

]]>
<![CDATA[ Okta sign out vs app sign out ]]> https://ishouldgotosleep.com/okta-sign-out-vs-app-sign-out/ 605f0b0aaba8710001105c16 Tips ]]> Thu, 03 Sep 2020 00:00:00 +0200 When you use OAuth2 with Okta to sign in users in your client app, you have two different sessions:

  • the Okta session, and
  • the client app session.

If you want to sign your user out, you need to close both sessions.

Closing the Okta session

You can close the Okta session by calling the logout endpoint.

If you use an SDK, there is probably a specific call for that.

oktaOidc.signOutOfOkta(authStateManager, from: self) { error in
    if let error = error {
        // Error
        return
    }
}

Closing the client app session

The client app session is closed by clearing tokens locally and revoking them, using the revoke endpoint.

Again, if you are using an SDK, there is a specific call for that.

authStateManager.revoke(authStateManager.refreshToken) { response, error in
    if let error = error else {
        // An error occurred
        return
    }
    // Token was revoked
}
]]>
<![CDATA[ Copy a single commit to another branch using cherry-pick ]]> https://ishouldgotosleep.com/copy-single-commit-to-another-branch-cherry-pick/ 605f0abeaba8710001105c09 Tips ]]> Wed, 02 Sep 2020 00:00:00 +0200 Today I was working on a new feature. I created a new branch feature/add-gallery-button.

While a was working on it, I noticed some code that, if improved, would have made the development process much easier. So I improved it.

Once I was done, I committed the code and pushed to the remote. I started a merge request and assigned it to another developer.

Then, after a celebration coffee, I started working on yet another new feature, feature/update-log-component, and I wanted to use the code improvements from feature/add-gallery-button.

Instead of copying the code manually, thus creating two commits in the repository history with the same change, it would have been nice to bring the commit from feature/add-gallery-button to feature/update-log-component.

This is possible with git cherry-pick.

You only need the SHA of the commit you want to copy, let's say 4350a96303c474812523c1214f50ffda1f2e437a.

Be sure to checkout the branch you are working on now.

git checkout feature/update-log-component
git cherry-pick 4350a96303c474812523c1214f50ffda1f2e437a

en voilร , the improved code is also on my current branch.

]]>
<![CDATA[ Distribute private custom iOS apps ]]> https://ishouldgotosleep.com/distribute-private-custom-apps/ 605f0a57aba8710001105bff Tips ]]> Tue, 01 Sep 2020 00:00:00 +0200 A video from WWDC2019 called App Distribution โ€“ From Ad-hoc to Enterpriseexplains the different distribution possibilities for iOS apps.

An interesting one is Custom Apps. According to the video, you can prepare the app as usual on App Store Connect, and associate a Customer ID to it, so that you can distribute it to their employees.

Once the app is available to your customer, they can use an MDM solution to make it available on their devices, or they can use redemption codes, so that the employees, can use their own devices.

I wonder if it can be used by your customer to distribute an app to their clients, via redemption codes.

There is another video, from WWDC2020, called Custom app distribution with Apple Business Manager, but I still need to watch it.

]]>
<![CDATA[ Different URL between Okta OAuth 2.0 login and logout ]]> https://ishouldgotosleep.com/okta-endpoint-url-after-login/ 605f0a20aba8710001105bf5 Tips ]]> Mon, 31 Aug 2020 00:00:00 +0200 The Okta API documentation mentions the same ${baseUrl} for all the endpoints. For instance, ${baseUrl}/v1/authorize to login, ${baseUrl}/v1/logout to logout, and ${baseUrl}/v1/userinfo for user information.

While working for a client's project, after a successful login, I started receiving a 404 not found error for other endpoints, like /logout and /userinfo.

It looks like some Okta implementations require a different baseUrl, in this client's specific case it's oauth2/.

Thus, if you get a 404 not found error for your logout endpoint, try to add oauth2/to the URL: https://${baseUrl}/oauth2/v1/logout.

]]>
<![CDATA[ Dart spread operator ]]> https://ishouldgotosleep.com/dart-spread-operator/ 605f09d1aba8710001105be7 Dart, Tips ]]> Fri, 28 Aug 2020 00:00:00 +0200 Dart supports the spread operator, which allows to insert multiple elements into a collection.

For instance, from the cascade I described yesterday, this

return someVariable.toList()
    ..add(anotherObject)
    ..addAll(anotherListOfObjects);

becomes

return [
    ...someVariable.toList(),
    anotherObject,
    ...anotherListOfObjects
];

Even more concise and easier to read.

]]>
<![CDATA[ Dart cascades ]]> https://ishouldgotosleep.com/dart-cascade-operator/ 605f0969aba8710001105bda Dart, Tips ]]> Thu, 27 Aug 2020 00:00:00 +0200 Cascades (..) allow for performing a sequence of operations on the same object.

The following code

Cart updateCart(Cart cart, List<Product> products, Discount discount) {
    var updatedCart = cart.adding(products);
    return updatedCart.applying(discount);
}

becomes

Cart updateCart(Cart cart, List<Product> products, Discount discount) {
    return cart.adding(products)
        ..applying(discount);
}

or adding elements to a List

var finalList = someVariable.toList();
finalList.add(anotherObject);
finalList.addAll(anotherListOfObjects);
return finalList

becomes

return someVariable.toList()
    ..add(anotherObject)
    ..addAll(anotherListOfObjects);

At first I found them hard to read. But after using them a couple of times, I got used to them.

]]>
<![CDATA[ Install Android Studio on an external SSD for Flutter ]]> https://ishouldgotosleep.com/install-android-studio-external-ssd-flutter/ 605f08a1aba8710001105bbb Tips ]]> Wed, 26 Aug 2020 00:00:00 +0200 When I work in the office I use a desktop with a one terabyte Fusion drive. That is plenty of storage for my development needs. On the laptop I use when I work from home, on the other hand, the available storage is low. Too low to run all the development tools I need.

For this reason, I decided to install Android Studio on an external SSD.

I downloaded it from the official website, moved it to the SSD, which I named Skynet, and launched it.

During installation I chose a custom setup, to set the location of the Android SDK to a folder in the SSD.

Then I run flutter doctor, but I got the error Android license status unknown.

So I run flutter doctor --android-licenses, which gave me a very weird message.

flutter doctor --android-licenses
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 5 more

After a quick Internet search, I opened Android Studio and clicked Configure.

And I added two packages:

  • NDK (Side by Side)
  • Android SDK Command-line Tools (latest)

I guess only the command-line tools are needed.

At this point flutter doctor --android-licenses worked as expected, but flutter doctor still complained that Android Studio was not installed.

โžœ  ~ flutter doctor                   
Doctor summary (to see all details, run flutter doctor -v):
[โœ“] Flutter (Channel stable, 1.20.1, on Mac OS X 10.15.6 19G2021, locale en)

[โœ“] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[โœ“] Xcode - develop for iOS and macOS (Xcode 11.6)
[!] Android Studio (not installed)
[โœ“] VS Code (version 1.48.2)
[!] Connected device

! Doctor found issues in 2 categories.

So I run flutter config --android-studio-dir /Volumes/Skynet/Android\ Studio.app, and finally, no errors.

โžœ  ~ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[โœ“] Flutter (Channel stable, 1.20.1, on Mac OS X 10.15.6 19G2021, locale en)

[โœ“] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[โœ“] Xcode - develop for iOS and macOS (Xcode 11.6)
[โœ“] Android Studio (version 4.0)
[โœ“] VS Code (version 1.48.2)
[!] Connected device

! Doctor found issues in 1 category.

๐Ÿฅณ

]]>
<![CDATA[ Stopping HealthKit observer queries ]]> https://ishouldgotosleep.com/stopping-healthkit-observer-queries/ 605f06deaba8710001105bad Tips ]]> Tue, 25 Aug 2020 00:00:00 +0200 Apple documentation about Executing Observer Queries says that "To stop the query, call the HealthKit storeโ€™s stopQuery: method.".

That method takes the query you want to stop as a parameter. Which is ok if you have access to the query.

But what if you created the query in a previous run of the app, and it has been running happily in the background since?

You cannot persist the query, like you would do with an HKQueryAnchor.

My solution was to persist the hash value of the query and associate with it whether the query should be stopped or not. Then, in the updateHandler of the query, I stop the query, if necessary. This is possible because the updateHandler has the query as one of the parameters.

let query = HKObserverQuery(sampleType: sampleType, predicate: nil) { (query, completionHandler, errorOrNil) in

    if let error = errorOrNil {
        // Properly handle the error.
        completionHandler()
        return
    }
    
    // Check if query should be stopped.
    if shouldStopQuery(queryHash: query.hash) {
        HKHealthStore.stop(query)
        completionHandler()
        return
    }


    // Execute other queries to access the new data.

    completionHandler()
}
]]>
<![CDATA[ Scheduled Firestore backups ]]> https://ishouldgotosleep.com/scheduled-firestore-backups/ 605eec7faba8710001105ac9 Tips ]]> Mon, 24 Aug 2020 00:00:00 +0200 I followed the official guide to create a scheduled Function on Firebase to backup Firestore on a Google Cloud bucket.

The code includes

...
const firestore = require('@google-cloud/firestore')
const client = new firestore.v1.FirestoreAdminClient()
...

but then, on pre-deploy, I got the error

Module '@google-cloud/firestore' is not listed as dependency in package.json.

And adding "@google-cloud/firestore": "^0.x.y" to package.json did not seem to be the right thing to do, because that package is already included in the Firebase sdk.

So I adapted the code from the team at Firebase to:

import * as admin from 'firebase-admin'
const client = new admin.firestore.v1.FirestoreAdminClient({})
...

and removed"@google-cloud/firestore": "^0.x.y" from package.json. This worked.

]]>
<![CDATA[ Swift Optional vs Dart sound null safety ]]> https://ishouldgotosleep.com/swift-optional-vs-dart-sound-null-safety/ 605efd3caba8710001105b8b Dart ]]> Sun, 23 Aug 2020 00:00:00 +0200 Recently the Dart team announced sound null safety. Dart was already a type-safe language, and now it is even better.

Sound null safety means that if a variable is not declared as nullable, then, whenever you access that variable, you will never find null, for its entire lifetime. This should remind you of Swift Optional.

Dart sound null safety and Swift Optional have a lot in common, but they are implemented in different ways. In Swift, Optional is a generic type, which means that, when we write var x: String?, what we mean is var x: Optional<String>: x is a variable that can contain a String or nil.

The Dart team took a different approach.

When you write String? x, the meaning is the same, x is a variable that can contain a String or null, but String? is a union type, it describes a value that can be one of several types.

In Dart, String is called a non-nullable type, while String? is a nullable type. In the rest of the article, we are going to refer to nullable types in Dart and to Optional in Swift as optionals, or optional types.

The two implementations are different, but they have a lot in common.

Optional binding

In Swift you can bind the value of an optional to a new variable, if that value is not nil, using an if let statement or a guard let:

// Swift

var x: String?

... // Assign some values or nil to x.

// print(x.count) // This does not compile.

guard let nonOptionalX = x else {
    return
}

// nonOptionalX is of type String, not String?.
print(nonOptionalX.count) // This will compile.

In Dart, the same behavior can be obtained by simply checking if the variable is null:

// Dart

String? x;

... // Assign some values or null to x.

// print(x.length); // This would NOT compile.

if (x == null) {
    // x is `null`
    return;
}
    
// x is non-null, and the compiler knows that.
print(x.length); // This will compile.

Optional chaining

Optional chaining is used to safely access properties or methods of an optional, and it is the same in both languages. It was already available in Dart before the introduction of sound null safety.

// Swift

var x: String?

... // Assign some values or `nil` to x.

// let length = x.count // This does not compile.

let length = x?.count 
// length is an Int? and it contains either nil, or the length of the string in x

// Dart

String? x;

... // Assign some values or null to x.

// var length = x.length; // This would NOT compile.

var length = x?.length; 
// length is an Int? and it contains either null, or the length of the string in x

Nil-coalescing operator

As per the optional chaining, the nil-coalescing operator is available in both languages, and it was also available before the introduction of sound null safety.

// Swift

var x: Bool?

... // Assign some values or `nil` to x.

print(x ?? false) // prints either the value in x, or 'false' if x is nil.

// Dart

bool? x;

... // Assign some values or null to x.

print(x ?? false); // prints either the value in x, or 'false' if x is nil.

late keyword

In Dart, you can use the late keyword in front of a non-nullable property, to inform the compiler that that property is not initialized immediately, but you will assigne a non-null value to it before accessing it.

From the official article by the Dart team:

// Dart

class Goo {
    late Viscosity v;
    
    Goo(Material m) {
        v = m.computeViscosity();
    }
}

In my opinion, you should avoid this use of late as much as possible. Use an Optional instead. The code might be clear to you while you write it, but it lacks in maintainability.

You can also use late on a property that has an initializer:

// Dart

class Goo {
    late Viscosity v = _m.computeViscosity();
}

In this case, the property v becomes lazy and it is initialized only when it is accessed for the first time. This is the same as using lazy in Swift:

// Swift

class Goo {
    lazy var v = m.computeViscosity()
}

Extending the optional type

In both languages, we can extend the functionalities of the optional type:

// Swift

extension Optional where Wrapped == Bool {
    /// Returns the value, or `false` if the value is `nil`
    func orFalse() -> Bool {
        return self ?? false
    }
}

let x: Bool? = nil
print(x.orFalse()) // prints `false`.
// Dart

extension UnwrapOrFalse on bool? {
    /// Returns the value, or `false` if the value is `null`
    bool orFalse() {
        return this ?? false;
    }
}

bool? x;
print(x.orFalse()); // prints `false`.

How programming in Dart will change

I am a big fan of Swift Optional. I like that the developer is forced to specified when a certain variable or parameter could be nil. Having a very similar feature in Dart will be welcomed by the entire community.

Say goodbye to all those, now useless, asserts on initializers and method definitions.

// Dart

Article({this.id, this.description}):
    assert(id != null),
    assert(description != null);
]]>
<![CDATA[ Publish a Swift static website on GitLab Pages ]]> https://ishouldgotosleep.com/publish-swift-static-website-gitlab/ 605f123faba8710001105d16 Tutorials ]]> Sun, 16 Aug 2020 00:00:00 +0200 One of the reasons why static websites are so hot right now is hosting prices.

There are many ways to host a static website for free, you can use the free tier of Netlify, GitHub Pages, Firebase Hosting, Amazon S3, and many others. If you already use GitLab to host the code of your projects, you can use GitLab Pages.

In this article, we will set up a new static website generated by Swift Publish and hosted by GitLab.

Create a repository on GitLab

The name and location you use to create your repository that will contain your website source code will dictate the URL of the website in GitLab Pages.

On GitLab.com if you create the project in your user space and name it using your username followed by .gitlab.io, like in [YOUR_USERNAME].gitlab.io, then it will be published on https://[YOUR_USERNAME].gitlab.io. If you name it anything else, like in [ANYTHING_ELSE] it will be published on

https://[YOUR_USERNAME].gitlab.io/[ANYTHING_ELSE].

If you create it in a group and name it [GROUP_NAME].gitlab.io, then the website will be available at https://[GROUP_NAME].gitlab.io.

On a self-hosted GitLab instance, the URL depends on the instance settings. GitLab pages could even be disabled on a self-hosted instance. You should contact the instance administrator if you are in doubt.

In this article, we will create a project named DeliciousRecipes on my user space, so it will be available on https://mvolpato.gitlab.io/deliciousrecipes.

Go to GitLab.com (or to your self hosted GitLab instance) and log in with your account. Click on New Project, type "DeliciousRecipes" as project name, leave the project slug as it is, and add a description. You can create a private project if you want, the visibility of the website does not depend on the visibility of the project.

Finally, click Create Project.

Init a new Swift Publish website

To create a new website, we follow the official quick start guide.

Download and build tools

Open Terminal and run

git clone https://github.com/JohnSundell/Publish.git
cd Publish
make

As described in the official guide, you might need to install the Xcode command-line tools:

xcode-select --install

Init your website source code

Create a new folder and run publish new in it:

mkdir DeliciousRecipes
cd DeliciousRecipes
publish new

Now you can run publish run in the same folder, and the project will build, fetching all the needed dependencies. When it's done, the website should be available at http://localhost:8000. If not, check the logs from the publish run command, it could be available on a different port.

Add GitLab CI configuration

GitLab Pages use GitLab CI/CD to (possibly) build your static website and deployit. We could just use it to deploy the website, but we also want to build it using GitLab CI/CD, because, then, we do not need to keep the compiled HTML and CSS in the repository.

We start by creating a new file, called .gitlab-ci.yml, in the root of our project:

touch .gitlab-ci.yml
open .gitlab-ci.yml

The content of the file should be:

# (1)
image: swift:5.2.4

# (2)
before_script:
  - swift build

pages:
  script:
    # (3)
    - swift run
    # (4)
    - mv Output public
  artifacts:
    paths:
      # (5)
      - public
  only:
    # (6)
    - master

where we instruct GitLab CI/CD to (1) use the Swift Docker container with Swift 5.2.4, which is the latest at the time of writing, then to (2) build the project so that all the dependencies are downloaded.

After the project is built, we (3) run it, so that the static website pages are generated, and we (4) move the assets from the Output directory to the publicdirectory, which will be used by GitLab as the directory containing the assets to deploy.

In (5) we define the path to the artifact generated by GitLab CI/CD. It looks like this cannot be changed to a custom folder.

Finally, we also (6) specify that the website should be published only when we push to the master branch so that we can use other branches for work in progresson the website, for new pages or articles.

If you use the SassPublishPlugin or the MagickPublishPlugin, you can change the Docker image to ishouldgotosleep/swift-publish:latest, which is described in another article, and it includes the tools to run those plugins.

# (1)
image: ishouldgotosleep/swift-publish:latest

Push the repository

Finally, we link the repository in GitLab and what we have been creating locally on our machine. Go to your project page: https://gitlab.com/[YOUR_GITLAB_USERNAME]/deliciousrecipes and follow the instructions in Push an existing folder:

git init
git remote add origin git@gitlab.com:[YOUR_GITLAB_USERNAME]/deliciousrecipes.git
git add .
git commit -m "Initial commit"
git push -u origin master

Now if you reload the page you will see that a pipeline is running:

and if you click on the small blue pipeline indicator, you can see that it is a GitLab Pages job.

Click on the pages job indicator to get to the detail page of the job, where you can see what is happening in the Docker container that is generating the website:

In the picture above, the worker that picked up the pages job is downloading the Docker container that we specified in the configuration file.

After a couple of minutes the job should be done:

Check the result

Go to https://[YOUR_GITLAB_USERNAME].gitlab.io/deliciousrecipes/ and you are greeted with a webpage without style:

This happens because the default theme does not support (yet) a website hosted in a subfolder. We need to use a different theme. For instance CasperishTheme, by Arnaud Joubay.

Use a different theme

We just need to add the theme to our dependencies in Package.swift

// swift-tools-version:5.1

import PackageDescription

let package = Package(
  name: "DeliciousRecipes",
  products: [
    .executable(name: "DeliciousRecipes", targets: ["DeliciousRecipes"])
  ],
  dependencies: [
    .package(url: "https://github.com/johnsundell/publish.git", from: "0.3.0"),
    .package(url: "https://github.com/sowenjub/CasperishTheme.git", .branch("master")) // <-- added this
  ],
  targets: [
    .target(
      name: "DeliciousRecipes",
      dependencies: ["Publish", "CasperishTheme"] // <-- changed this
    )
  ]
)

and use the example they provide in their GitHub repository in our main.swift:

import Foundation
import Publish
import Plot
import CasperishTheme

// This type acts as the configuration for your website.
struct DeliciousRecipes: Website, CasperishWebsite {
    enum SectionID: String, WebsiteSectionID {
        // Add the sections that you want your website to contain here:
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata, CasperishWebsiteItemMetadata {
        // Add any site-specific metadata that you want to use here.
        var cover: String?
    }

    // Update these properties to configure your website:
    var url = URL(string: "https://mvolpato.gitlab.io/deliciousrecipes/")!
    var name = "MyWebsite"
    var description = "A description of MyWebsite"
    var language: Language { .english }
    var imagePath: Path? { nil }

    // Update these properties to configure your casperish-website:
    var rootPathString = "deliciousrecipes"
    var headerColor = "#424242"
    var cover = ""
    var author = "Your name"
    var avatar = ""
    var bio = "Bio"
    var icon = "๐Ÿ"
    var newsletterAction = ""
    var contacts: [(ContactPoint, String)] { [] }
}

// This will generate your website using the Casperish theme:
try DeliciousRecipes().publish(
    withTheme: .casperish,
    additionalSteps: [
        .installPlugin(.readingTime()),
        .installPlugin(.pygments()),
    ],
    plugins: [.pygments()]
)

Push and visit again https://[YOUR_GITLAB_USERNAME].gitlab.io/deliciousrecipes/ and now a nice styled website should appear.

Ignore Output folder

Now that we build the website in the Docker container via GitLab CI, we can delete the Output folder from the project and ignore it in .gitignore:

.DS_Store
/build
/.build
/.swiftpm
/*.xcodeproj
.publish
Output

Resources

This is a link to the repository used in this article, while at this link you can find the final published website.

The enhanced Docker image, that can use the SassPublishPlugin and the MagickPublishPlugin, can be found on Docker Hub.

]]>
<![CDATA[ Create a Swift Docker image to build a static website using Publish ]]> https://ishouldgotosleep.com/swift-docker-image-publish-static-website/ 605f11eeaba8710001105d09 Tutorials ]]> Mon, 10 Aug 2020 00:00:00 +0200 The typical workflow of working on a static website with Publish is, usually,

  • use publish run locally to get a localhost web server;
  • work on the content, theme, or building process on (possibly) Xcode;
  • compile the latest changes;
  • see the changes on the localhost;
  • go live with the changes.

When you are ready to go live with your website, you are faced with a choice: building your website locally to upload the output on a hosted space, or having some continuous deployment tool take care of the building process and upload.

Locally building and uploading

When you run publish run, an Output directory is created, with the compiled version of your website. You can manually upload the content of this directory to where your website is hosted.

Publish facilitates the deployment process by including a publish deploycommand.

The publish deploy command can be run on your machine and everything should work fine, because you already have all the dependencies and external tools that you need to compile the website during development.

How to avoid building locally

If, instead, you run publish deploy from your continuous deployment tool, you will need to use an environment that includes all your needed dependencies and external tools.

The best way to ensure that the machine used to build your website has all the needed tools is by using a Docker container.

A Docker container is a lightweight, versioned, piece of software that is independent from the environment that runs it. This independence is exactly what we are looking for.

We want a container that is able to compile Swift packages, plus the external tools that we need for our specific website.

Once we define such a container, we can use continuous deployment tools to load the container and compile/deploy our website from it. It does not matter when we compile, or where we compile it, the output code for our website will be the sameas long as we use the same container.

The Dockerfile

A Docker container is defined by a Docker image. You can have multiple containers running at the same time, defined by the same Docker image.

A Docker image is built from a versioned file called Dockerfile.

In our case, the image needs to include a version of Swift and all the external tools we need for compiling the website. We define tools and versions in the Dockerfile.

A very basic Dockerfile that we can use is

FROM swift:5.2.4

which says: use the base Docker image, maintained by the Swift team, that has Swift version 5.2.4, and do not add anything to it.

Such Dockerfile is not very interesting, it does not add anything to that base image, thus we could use that image directly.

But if we need some external tool like LibSass to compile sass code into css with the SassPublishPlugin, or if we modify images in building time with ImageMagick using MagickPublishPlugin, we need to add the tools in the Docker image.

A much more interesting Dockerfile would be

FROM swift:5.2.4

# Needed libraries and programs 
RUN apt-get update && apt-get install -y \
libsass-dev \
imagemagick \
&& rm -rf /var/lib/apt/lists/* \

where, when compiling the Docker image, we install libsass-dev and imagemagick.

Every container launched from this image will include Swift, LibSass, and ImageMagick, so it will be ready to compile our website.

Using the container

The Docker image defined by the Dockerfile above can be used in continuous deployment tools like GitHub Actions, or GitLab CI/CD.

For instance it can be used as starting image in this article about using GitLab CI/CD to publish Swift static websites.

You can build the image every time you need it, that is, every time you push new changes to your remote repository, or, you can use the image I already uploaded to Docker Hub so you do not need to maintain the Dockerfile.

If you need a special Docker file, you can even start from my image and add more external tools. Then you can publish your own version on Docker Hub, a big repository of images that everybody can easily use:

FROM ishouldgotosleep/swift-publish:5.2.4

# Needed libraries and programs 
RUN apt-get update && apt-get install -y \
# Your tools and libraries here
&& rm -rf /var/lib/apt/lists/* \
]]>
<![CDATA[ Working with drafts in Publish ]]> https://ishouldgotosleep.com/working-withdraft-publish/ 605f11aaaba8710001105cf5 Tutorials ]]> Mon, 13 Jul 2020 00:00:00 +0200 Publish is a static site generator for Swift developers. You create Markdown files, and then generate the entire website by running a simple sequence of steps like:

try DeliciousRecipes().publish(using: [
    .addMarkdownFiles(),
    .copyResources(),
    .addFavoriteItems(),
    .addDefaultSectionTitles(),
    .generateHTML(withTheme: .delicious),
    .generateRSSFeed(including: [.recipes]),
    .generateSiteMap()
])

If you want to work with draft articles, that is, you want to keep some articles hidden until they are ready, you can easily use branches.

But you can also do it directly in Publish, by creating a custom step.

Prepare your draft article

You need to mark an article as draft, by adding the draft metadata in the front matter:

---
date: 2020-07-13 21:00
...
draft: true
---

and you need to specify it in the WebsiteItemMetadata struct:

struct ItemMetadata: WebsiteItemMetadata {
    var draft: Bool?
}

Define a new step

Defining a new step is easy, in main.swift add:

extension PublishingStep where Site == YourWebsiteName {
    static func removeDrafts() -> Self {
        .step(named: "Remove drafts") { context in
            context.mutateAllSections { section in
                section.removeItems(matching: \.metadata.draft == true)
            }
        }
    }
}

This step will iterate over all section, removing all items that have the draft field, and the field is set to true.

Add the step to the publishing process

Finally you can update the publishing process with the new step.

try DeliciousRecipes().publish(using: [
    .addMarkdownFiles(),
    .removeDrafts(), // <-- we added this.
    .copyResources(),
    .addFavoriteItems(),
    .addDefaultSectionTitles(),
    .generateHTML(withTheme: .delicious),
    .generateRSSFeed(including: [.recipes]),
    .generateSiteMap()
])

In this way you can define more sequences of steps, and choose which one you want to use, based on environment variables, or command line arguments. For instance, you can keep the draft for development, and remove them for deploy in your CI/CD tool.

]]>
<![CDATA[ Save the setup of your development machine ]]> https://ishouldgotosleep.com/save-development-setup-machine/ 605eefa5aba8710001105ae2 Programming ]]> Thu, 09 Jul 2020 00:00:00 +0200 Sometimes it happens. You buy a new iMac, or a new version of macOS is out and you want a clean installation, or you start a new job, and you get a new MacBook pro. Now you need to set it up.

The set up can be speed up if you keep a list of tasks to perform on a new machine, with scripts and dotfiles, in a repository hosting service like GitHub or GitLab.

These are the tasks and scripts that I use to setup my development machine.

Copy files from backup

The first step in my list of tasks is to copy some essential files from a backup, or from an existing machine:

cd FOLDER_FROM_BACKUP/
cp -r .ssh .gnupg .env .oh-my-zsh $HOME/

Then I clone the repository with my scripts and dotfiles. Run an bootstrap script that will setup my git configuration

# Install Xcode Command Line Tools
xcode-select --install

# Copy configuration files to home
cp -r .gitconfig .zshrc $HOME/

# Force disable .DS_Store
git config --global core.excludesfile $HOME/.gitignore
echo .DS_Store >> $HOME/.gitignore

Setup scripts

I install Homebrew and other tools, with a simple script.

# This installs homebrew itself, and also the command line tools in silent mode
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# Disable brew analytics
brew analytics off

brew update
brew upgrade

BREW_PREFIX=$(brew --prefix)

brew tap caskroom/cask-cask
brew tap homebrew/services

brew install curl wget git

## oh-my-zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

Another script will install tools and application using brew.

brew update
brew upgrade

# iOS dev tools
brew install cocoapods swiftlint

# Manage AppStore via terminal
brew install mas

# Rclone to backup folders to cloud services
brew install rclone

# Autojump allows to cd into a directory with `j DIRECTORY_NAME`
brew install autojump

### Cask
# These tools should update themselves, so no need to update them via Homebrew

# Visual Studio Code
brew cask install visual-studio-code 

# Firefox
brew cask install firefox

# Brave
brew cask install brave

# Google Chrome
brew cask install google-chrome

# Proxyman
brew cask install proxyman

# Paw.cloud
brew cask install paw

# Docker
brew cask install docker

# RSS reader
brew cask install netnewswire

# final cleanup
brew cleanup

Rclone helps you syncing files to cloud services, while encrypting them. I also need to get the configuration file from a backup.

cp FOLDER_FROM_BACKUP/rclone.conf $HOME/.config/rclone/

Many configurations of macOS can also be set via scripts.

# close any system preference windows first
osascript -e 'tell application "System Preferences" to quit'

# become root
sudo -v

# stay root
while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &

###############################################################################
# System                                                                      #
###############################################################################

# Enable Firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on

###############################################################################
# General UI/UX                                                               #
###############################################################################

# Expand save panel by default
defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode -bool true
defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 -bool true

# Expand print panel by default
defaults write NSGlobalDomain PMPrintingExpandedStateForPrint -bool true
defaults write NSGlobalDomain PMPrintingExpandedStateForPrint2 -bool true

# Save to disk (not to iCloud) by default
defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false

# Disable the โ€œAre you sure you want to open this application?โ€ dialog
defaults write com.apple.LaunchServices LSQuarantine -bool false

# Reveal IP address, hostname, OS version, etc. when clicking the clock
# in the login window
sudo defaults write /Library/Preferences/com.apple.loginwindow AdminHostInfo HostName

# Disable automatic capitalization as itโ€™s annoying when typing code
defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false

# Disable smart dashes as theyโ€™re annoying when typing code
defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false

# Disable automatic period substitution as itโ€™s annoying when typing code
defaults write NSGlobalDomain NSAutomaticPeriodSubstitutionEnabled -bool false

# Disable smart quotes as theyโ€™re annoying when typing code
defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false

# Disable auto-correct
defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false

# Set language and text formats
# Note: if youโ€™re in the US, replace `EUR` with `USD`, `Centimeters` with
# `Inches`, `en_GB` with `en_US`, and `true` with `false`.
defaults write NSGlobalDomain AppleLanguages -array "en" "nl"
defaults write NSGlobalDomain AppleLocale -string "en_GB@currency=EUR"
defaults write NSGlobalDomain AppleMeasurementUnits -string "Centimeters"
defaults write NSGlobalDomain AppleMetricUnits -bool true

# Show language menu in the top right corner of the boot screen
sudo defaults write /Library/Preferences/com.apple.loginwindow showInputMenu -bool true

# Set the timezone; see `sudo systemsetup -listtimezones` for other values
sudo systemsetup -settimezone "Europe/Brussels" > /dev/null

###############################################################################
# Screen                                                                      #
###############################################################################

# Require password immediately after sleep or screen saver begins
defaults write com.apple.screensaver askForPassword -int 1
defaults write com.apple.screensaver askForPasswordDelay -int 0

# Save screenshots to the desktop
defaults write com.apple.screencapture location -string "${HOME}/Desktop"

# Save screenshots in PNG format (other options: BMP, GIF, JPG, PDF, TIFF)
defaults write com.apple.screencapture type -string "png"

# Disable shadow in screenshots
defaults write com.apple.screencapture disable-shadow -bool true

###############################################################################
# Finder                                                                      #
###############################################################################

# Finder: show hidden files by default
defaults write com.apple.finder AppleShowAllFiles -bool true

# Finder: show all filename extensions
defaults write NSGlobalDomain AppleShowAllExtensions -bool true

# Finder: show status bar
defaults write com.apple.finder ShowStatusBar -bool true

# Finder: show path bar
defaults write com.apple.finder ShowPathbar -bool true

# Keep folders on top when sorting by name
defaults write com.apple.finder _FXSortFoldersFirst -bool true

# When performing a search, search the current folder by default
defaults write com.apple.finder FXDefaultSearchScope -string "SCcf"

# Disable the warning when changing a file extension
defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false

# Avoid creating .DS_Store files on network or USB volumes
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true

# Use list view in all Finder windows by default
# Four-letter codes for the other view modes: `icnv`, `clmv`, `glyv`
defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"

# Show the ~/Library folder
chflags nohidden ~/Library && xattr -d com.apple.FinderInfo ~/Library

# Show the /Volumes folder
sudo chflags nohidden /Volumes

# Expand the following File Info panes:
# โ€œGeneralโ€, โ€œOpen withโ€, and โ€œSharing & Permissionsโ€
defaults write com.apple.finder FXInfoPanesExpanded -dict \
General -bool true \
OpenWith -bool true \
Privileges -bool true

###############################################################################
# Dock, Dashboard, and hot corners                                            #
###############################################################################

# Enable highlight hover effect for the grid view of a stack (Dock)
defaults write com.apple.dock mouse-over-hilite-stack -bool true

# Donโ€™t automatically rearrange Spaces based on most recent use
defaults write com.apple.dock mru-spaces -bool false

# Donโ€™t show recent applications in Dock
defaults write com.apple.dock show-recents -bool false

# Hot corners
# Top left screen corner โ†’ Mission Control
defaults write com.apple.dock wvous-tl-corner -int 2
defaults write com.apple.dock wvous-tl-modifier -int 0
# Bottom left screen corner โ†’ Start screen saver
defaults write com.apple.dock wvous-bl-corner -int 5
defaults write com.apple.dock wvous-bl-modifier -int 0

###############################################################################
# Safari & WebKit                                                             #
###############################################################################

# Privacy: donโ€™t send search queries to Apple
defaults write com.apple.Safari UniversalSearchEnabled -bool false
defaults write com.apple.Safari SuppressSearchSuggestions -bool true

# Press Tab to highlight each item on a web page
defaults write com.apple.Safari WebKitTabToLinksPreferenceKey -bool true
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2TabsToLinks -bool true

# Show the full URL in the address bar (note: this still hides the scheme)
defaults write com.apple.Safari ShowFullURLInSmartSearchField -bool true

# Set Safariโ€™s home page to `about:blank` for faster loading
defaults write com.apple.Safari HomePage -string "about:blank"

# Prevent Safari from opening โ€˜safeโ€™ files automatically after downloading
defaults write com.apple.Safari AutoOpenSafeDownloads -bool false

# Enable Safariโ€™s debug menu
defaults write com.apple.Safari IncludeInternalDebugMenu -bool true

# Enable the Develop menu and the Web Inspector in Safari
defaults write com.apple.Safari IncludeDevelopMenu -bool true
defaults write com.apple.Safari WebKitDeveloperExtrasEnabledPreferenceKey -bool true
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled -bool true

# Add a context menu item for showing the Web Inspector in web views
defaults write NSGlobalDomain WebKitDeveloperExtras -bool true

# Warn about fraudulent websites
defaults write com.apple.Safari WarnAboutFraudulentWebsites -bool true

# Disable plug-ins
defaults write com.apple.Safari WebKitPluginsEnabled -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2PluginsEnabled -bool false

# Disable Java
defaults write com.apple.Safari WebKitJavaEnabled -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaEnabled -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaEnabledForLocalFiles -bool false

# Block pop-up windows
defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaScriptCanOpenWindowsAutomatically -bool false

# Enable โ€œDo Not Trackโ€
defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true

###############################################################################
# Mail                                                                        #
###############################################################################

# Copy email addresses as `foo@example.com` instead of `Foo Bar <foo@example.com>` in Mail.app
defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false

# Disable inline attachments (just show the icons)
defaults write com.apple.mail DisableInlineAttachmentViewing -bool true

###############################################################################
# Spotlight                                                                   #
###############################################################################

# Change indexing order and disable some search results
# Yosemite-specific search results (remove them if you are using macOS 10.9 or older):
#     MENU_DEFINITION
#     MENU_CONVERSION
#     MENU_EXPRESSION
#     MENU_SPOTLIGHT_SUGGESTIONS (send search queries to Apple)
#     MENU_WEBSEARCH             (send search queries to Apple)
#     MENU_OTHER
defaults write com.apple.spotlight orderedItems -array \
'{"enabled" = 1;"name" = "APPLICATIONS";}' \
'{"enabled" = 1;"name" = "SYSTEM_PREFS";}' \
'{"enabled" = 1;"name" = "DIRECTORIES";}' \
'{"enabled" = 1;"name" = "PDF";}' \
'{"enabled" = 1;"name" = "FONTS";}' \
'{"enabled" = 1;"name" = "DOCUMENTS";}' \
'{"enabled" = 0;"name" = "MESSAGES";}' \
'{"enabled" = 0;"name" = "CONTACT";}' \
'{"enabled" = 0;"name" = "EVENT_TODO";}' \
'{"enabled" = 0;"name" = "IMAGES";}' \
'{"enabled" = 0;"name" = "BOOKMARKS";}' \
'{"enabled" = 0;"name" = "MUSIC";}' \
'{"enabled" = 0;"name" = "MOVIES";}' \
'{"enabled" = 1;"name" = "PRESENTATIONS";}' \
'{"enabled" = 0;"name" = "SPREADSHEETS";}' \
'{"enabled" = 1;"name" = "SOURCE";}' \
'{"enabled" = 0;"name" = "MENU_DEFINITION";}' \
'{"enabled" = 0;"name" = "MENU_OTHER";}' \
'{"enabled" = 0;"name" = "MENU_CONVERSION";}' \
'{"enabled" = 0;"name" = "MENU_EXPRESSION";}' \
'{"enabled" = 0;"name" = "MENU_WEBSEARCH";}' \
'{"enabled" = 0;"name" = "MENU_SPOTLIGHT_SUGGESTIONS";}'

###############################################################################
# Terminal                                                                    #
###############################################################################

# Only use UTF-8 in Terminal.app
defaults write com.apple.terminal StringEncodings -array 4

# Enable Secure Keyboard Entry in Terminal.app
# See: https://security.stackexchange.com/a/47786/8918
defaults write com.apple.terminal SecureKeyboardEntry -bool true

###############################################################################
# Time Machine                                                                #
###############################################################################

# Prevent Time Machine from prompting to use new hard drives as backup volume
defaults write com.apple.TimeMachine DoNotOfferNewDisksForBackup -bool true

###############################################################################
# Activity Monitor                                                            #
###############################################################################

# Show the main window when launching Activity Monitor
defaults write com.apple.ActivityMonitor OpenMainWindow -bool true

# Visualize CPU usage in the Activity Monitor Dock icon
defaults write com.apple.ActivityMonitor IconType -int 5

# Show all processes in Activity Monitor
defaults write com.apple.ActivityMonitor ShowCategory -int 0

# Sort Activity Monitor results by CPU usage
defaults write com.apple.ActivityMonitor SortColumn -string "CPUUsage"
defaults write com.apple.ActivityMonitor SortDirection -int 0

###############################################################################
# Address Book, Dashboard, iCal, TextEdit, and Disk Utility                   #
###############################################################################

# Enable the debug menu in Address Book
defaults write com.apple.addressbook ABShowDebugMenu -bool true

# Use plain text mode for new TextEdit documents
defaults write com.apple.TextEdit RichText -int 0

# Open and save files as UTF-8 in TextEdit
defaults write com.apple.TextEdit PlainTextEncoding -int 4
defaults write com.apple.TextEdit PlainTextEncodingForWrite -int 4

# Enable the debug menu in Disk Utility
defaults write com.apple.DiskUtility DUDebugMenuEnabled -bool true
defaults write com.apple.DiskUtility advanced-image-options -bool true

###############################################################################
# Mac App Store                                                               #
###############################################################################

# Enable Debug Menu in the Mac App Store
defaults write com.apple.appstore ShowDebugMenu -bool true

# Enable the automatic update check
defaults write com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true

# Check for software updates daily, not just once per week
defaults write com.apple.SoftwareUpdate ScheduleFrequency -int 1

# Download newly available updates in background
defaults write com.apple.SoftwareUpdate AutomaticDownload -int 1

###############################################################################
# Photos                                                                      #
###############################################################################

# Prevent Photos from opening automatically when devices are plugged in
defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool true

echo "Done. Note that some of these changes require a logout/restart to take effect."

What about installing apps from the App Store? Also via a script.

# Download AppStore apps
mas install 1352778147 # Bitwarden
mas install 1333542190 # 1Password
mas install 803453959  # Slack
mas install 409203825  # Numbers

Avoid malicious connections

The host file can be filled with a list of hosts that are not trusted. The idea is that your machine will resolve malicious domains to an ip address that you know is not malicious. You can review the list of domains you want to add, and then run

# Download hosts file, this is extended with fakenews, gambling, porn
sudo curl -o /private/etc/hosts 'https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/YOUR_CHOICE_HERE/hosts'

# Reload hosts
sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder

Developer tools

Flutter

git clone https://github.com/flutter/flutter.git $HOME/Developer/flutter -b stable

# Quit terminal and test installation
which flutter
flutter doctor

The maintainers of NVM do not recommend to install it via Homebrew. You can use zsh-nvm

git clone https://github.com/lukechilds/zsh-nvm.git ~/.zsh-nvm
source ~/.zsh-nvm/zsh-nvm.plugin.zsh

and add

# NVM
export NVM_DIR=~/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

to ~/.zshrc.

Preferences

Some changes either need user interaction, or can only be done via UI. Like FileVault.

fdesetup status
# If not enabled: 
sudo fdesetup enable
# or enable it from the Settings app

Preferences > Sidebar and add

  • User home
  • System root

Additional Hints

Many more insights can be found in these lists:

]]>
<![CDATA[ Firestore rules testing on a custom port ]]> https://ishouldgotosleep.com/firestore-rules-testing-custom-port/ 605f1112aba8710001105cde Tutorials ]]> Sun, 05 Jul 2020 00:00:00 +0200 When working with Firebase, it is possible to emulate Firestore, and other Firebase services, on your local machine, so that you do not need to be connected to your production project, and, more importantly, you do not risk to change or delete data used in production.

In some situation you need to run the Firebase emulators on a different port than the default one.

You can do that by setting the port in firebase.json:

{
    "firestore": {
        "rules": "firestore.rules",
        "indexes": "firestore.indexes.json"
    },
    "emulators": {
        "firestore": {
            "port": 65535
            }
    }
}

then, in your app, you need to set the host in your Firestore.instance.settings to localhost:65535, and sslEnabled: false.

Firestore tests will not run

Unit testing of the Firestore security rules should be done locally, because, as Firebase guru Todd Kerpelman explains in this video, they are faster, safer, and cheaper:

What is not explained, however, is how to deal with custom configurations.

If you follow the video with Firestore running on a non-default port, you encounter this error:

Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=unavailable]: 14 UNAVAILABLE: No connection established

This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.

The solution

The explanation in the error message is not really helpful. In fact the problem is that you need to set up your tests to connect to your emulator:

describe("Our social app", () => {
    it("Can read items in the read-only collection", () => {
        const db = firebase.initializeTestApp({ projectId: MY_PROJECT_ID }).firestore()
        db.settings({ host: "localhost:65535", ssl: false });
        const testDoc = db.collection("readonly").doc("testDoc");
        qfirebase.assertSucceeds(testDoc.get());
    });
});

Cleaning the database when tests are finished

At the end of the tests, or even at the beginning of each test, you should clean the database. This can be done with

firebase.clearFirestoreData({projectId: MY_PROJECT_ID});

The problem is that this call tries to clear the data in the Firestore emulator on the default port, givin the error

connect ECONNREFUSED 127.0.0.1:8080

At this moment it is not possible to provide a port number to clearFirestoreData, so it is not possible to use that function to clear your local emulated Firestore.

The only solution is to manually remove the created data using the SDK:

after(async () => {
    const dbAdmin = firebase.initializeAdminApp({ projectId: MY_PROJECT_ID }).firestore();
    dbAdmin.settings({ host: "localhost:65535", ssl: false });
    await dbAdmin.collection("posts").doc("public_post").delete();
    await dbAdmin.collection("posts").doc("private_post").delete();
})

Just remember to delete document subcollections, if you created any, because they will not be deleted by deleting the parent document.

]]>
<![CDATA[ Modify images in Swift using ImageMagick ]]> https://ishouldgotosleep.com/resize-images-swift-imagemagick/ 605ee86baba8710001105a86 Tutorials ]]> Tue, 30 Jun 2020 00:00:00 +0200 When you are programming for iOS, and you need to modify images, you can use the UIKit framework. If you are working on a macOS scripting program, you do not have access to it.

You can use third party libraries, like jpeg and png, or you can use an external program, like ImageMagick.

We will use ImageMagick, first with Process, and then with ShellOut.

Setup your machine with ImageMagick

ImageMagick is a free software used to modify, resize, and convert image in many different formats. You can download it directly from the official website, but the preferred installation method is using Homebrew:

brew install imagemagick

which will install version 7. You can install version 6 if you experience problems. Version 6 is still maintained.

Together with magick, ImageMagick installs other command line tools. After the installation, in terminal run

which magick

to output the location of the ImageMagick binary, which is probably /usr/local/bin/magick. You will need it later.

Now we can resize an image with

magick mogrify -resize 50% path/to/image.jpg

Run the command line tool from Swift using Process

To run the command line tools from Swift, we will use Process, a class available in the Foundation framework.

let task = Process()

task.executableURL = URL(fileURLWithPath: "/usr/local/bin/magick")

task.arguments = [
    "mogrify",
    "-resize",
    "50%",
    "path/to/image.jpg"
]

try task.run()
task.waitUntilExit()

let status = task.terminationStatus

if status == 0 {
    fputs("Resizing succeeded.\n", stdout)
} else {
    fputs("Resizing failed.\n", stdout)
}

You can see how this is all implemented in version 0.1.0 of a plugin for Publish, a static site generator for Swift developers.

Run the command line tool from Swift using ShellOut

Instead of making use of Process directly, we can take advantage of existing packages, like ShellOut, which uses Process under the hood.

After adding ShellOut as a dependency, you can replicate the script above with

let arguments = [
    "mogrify",
    "-resize",
    "50%",
    "path/to/image.jpg"
]

do {
    try shellOut(to: "/usr/local/bin/magick \(arguments.joined(separator: " "))")
} catch {
    let error = error as! ShellOutError
    print(error.message)
    print(error.output)
}

This is implemented in version 0.2.0 of the plugin.

]]>