Recently I’ve been trying out Flutter to try to learn if and when I might want to reach for it on future projects.
I enjoy learning by reading books, and this time I worked through Programming Flutter by Carmine Zaccagnino, published by The Pragmatic Programmers. It’s a great book that really helped me get some Flutter foundations under my belt. (Note: this isn’t an affiliate link and we aren’t receiving anything for recommending it; it’s just a great book!)
There was just one challenge working through the book: it was written in 2020, and both the Flutter framework and the Dart programming language have undergone some changes since then. Some were fairly easy to get past, and some took a bit more work.
Because of these changes in Flutter, I wanted to write this post to share how I got past some errors.
It doesn’t cover 100% of the errors I ran across; for some errors, editor feedback provided enough information for to make the fixes needed. This post just calls out the changes that were the most frequent or took the most work to overcome.
I hope a second edition of Programming Flutter will be published with updates addressing what I’ll cover below, but until then I hope these notes are helpful.
Keys in constructors
Some of the examples of widgets defined in the book don’t have constructors if they don’t take in any data:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ...
}
}
When I write this code, I get an editor warning: Constructors for public widgets should now have a named ‘key’ parameter.
Saving the file should automatically correct this:
class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
@override
Widget build(BuildContext context) {
// ...
}
}
For widgets defined in the book that do have constructors, they typically don’t take in a key
, but it is now required for them too. Saving should automatically add the key
in that case as well.
const
constructors
Flutter build()
methods involve a lot of calls to widget constructors as you construct a tree of widgets to return. When I write a widget tree like this, I often see editor suggestions to add a const
keyword to some of these constructor calls. For example, for this snippet:
Card(
color: Colors.lightGreen[50],
child: Padding(
padding: EdgeInsets.all(15.0),
child: Text(
_str,
textScaleFactor: 2.0,
),
),
),
I get the editor feedback: Use ‘const’ with the constructor to improve performance.
Saving the file applies this suggestion automatically; the const
keyword is added:
Card(
color: Colors.lightGreen[50],
child: Padding(
- padding: EdgeInsets.all(15.0),
+ padding: const EdgeInsets.all(15.0),
child: Text(
_str,
textScaleFactor: 2.0,
),
),
),
[Note: This code snippet has a second unrelated bit of editor feedback: textScaleFactor
is deprecated. To fix that, try replacing it with textScaler: const TextScaler.linear(2.0)
.]
When you’re defining your own widgets, they may benefit from being constant as well, in which case the constructor should have a const
keyword as well. You may get the editor feedback Constructors in ‘@immutable’ classes should be declared as ‘const’.
When you do, saving results in that keyword being added automatically.
There’s one more situation related to const
that I ran into while working through the book. Sometimes, based on changes I made, a widget that was previously const
could no longer be so, resulting in an error. I don’t recall exactly what the error was, and it wasn’t auto-fixable, but the error was clear enough that I could tell I should manually remove the const
keyword. When I did, the code worked fine.
HTTP methods now take a Uri
The book includes calls to http.read()
that take the URL as a string:
String responseBody = await http.read("http://example.com");
When I do so, I get the error The argument type ‘String’ can’t be assigned to the parameter type 'Uri'.
This is because version 0.13.0
of the http
library removed the APIs that allowed passing in a String
directly. Instead, you need to pass in a Uri
object. The simplest way to get one is to use the Uri.parse()
method to convert a string to a Uri
:
String responseBody = await http.read(Uri.parse("http://example.com"));
FlatButton no longer exists
The book uses the FlatButton
widget as the default button with a text label. However, in 2021, the FlatButton
and a few other button widgets were deprecated and replaced by new widgets. For example, consider this snippet:
FlatButton(
onPressed: _resetCounter,
color: Colors.red,
child: Text(
"Reset counter",
style: Theme.of(context).textTheme.labelLarge,
),
),
This snippet gives me the error The method 'FlatButton' isn't defined for the type...
.
The most closely equivalent of the new button widgets is TextButton
. However, just changing FlatButton
to TextButton
in this case results in another error: The named parameter ‘color’ isn’t defined.
This is because the approach for styling the background color has changed too.
Whereas with FlatButton
we simply passed a color
to it, with TextButton
we pass a style
pointing to the return value of TextButton.styleFrom()
. The backgroundColor:
argument can be passed to set the background color:
-FlatButton(
+TextButton(
onPressed: _resetCounter,
- color: Colors.red,
+ style: TextButton.styleFrom(backgroundColor: Colors.red),
child: Text(
"Reset counter",
style: Theme.of(context).textTheme.labelLarge,
),
),
Null safety
In the book, many of the variables have null
values at times. In Dart 3, however, null safety is required by the language. As a result, by default variables do not permit null
values; if a variable needs to be able to contain a null
value, it must be explicitly annotated as such with the ?
suffix. When you do, the compiler will force you to handle the possibility of a null value.
For example, this snippet implements a FutureBuilder
where, if a comic has been returned, the data in the response is passed to a ComicTile
widget:
FutureBuilder(
future: fetchComic(i),
builder: (context, comicResult) => comicResult.hasData
? ComicTile(comic: comicResult.data)
: const Center(...),
),
The book gives the following start of the definition of ComicTile
:
class ComicTile extends StatelessWidget {
ComicTile({this.comic});
final Map<String, dynamic> comic;
//...
}
However, this gives several errors. First, the issues mentioned earlier about the const
keyword and constructor keys. Saving results in fixes applied for both of these:
class ComicTile extends StatelessWidget {
- ComicTile({this.comic});
+ const ComicTile({super.key, this.comic});
final Map<String, dynamic> comic;
//...
}
However, there is one remaining error that isn’t autofixable: The parameter 'comic' can’t have a value of 'null' because of its type, but the implicit default value is 'null'. Try adding either an explicit non-null default value or the 'required' modifier.
As the error message suggests, there are a few options for how to handle this. First, we can change the type of comic
to indicate that it can be null:
class ComicTile extends StatelessWidget {
const ComicTile({super.key, this.comic});
- final Map<String, dynamic> comic;
+ final Map<String, dynamic>? comic;
//...
}
If we do so, though, Dart will ensure we account for the possibility of null
values, and we have to check for that or use safe navigation operators (NAME?) like ?
.
In this case, this probably isn’t the best solution, because we only render the ComicTile()
widget when we do have comic data. Since we do know the value will be present, we can use the required
keyword to tell Dart this:
class ComicTile extends StatelessWidget {
- const ComicTile({super.key, this.comic});
+ const ComicTile({super.key, required this.comic});
final Map<String, dynamic> comic;
//...
}
However, if we do this, we will get a warning at the point of use. Here’s the snippet again:
FutureBuilder(
future: fetchComic(i),
builder: (context, comicResult) => comicResult.hasData
? ComicTile(comic: comicResult.data)
: const Center(...),
),
The error we get is on the access of comicResult.data
, and it says The argument type 'Map<String, dynamic>?' can't be assigned to the parameter type 'Map<String, dynamic>'.
In other words, we are trying to pass an optional value at a place a non-optional value is needed. But didn’t we already check that hasData
is true? Yes, but as far as the type system is concerned, the data
argument can still be null
at that point.
First I tried adding an explicit not-null check to see if that would satisfy the type system:
FutureBuilder(
future: fetchComic(i),
- builder: (context, comicResult) => comicResult.hasData
+ builder: (context, comicResult) => comicResult.hasData && comicResult.data != null
? ComicTile(comic: comicResult.data)
: const Center(...),
),
Unfortunately, this wasn’t enough for Dart to be confident that comicResult.data
was not null. One way to fix it is to use !
to tell the compiler to treat the value as not-null. This isn’t always safe to do, but in this case where we’ve just checked that it’s not null, it can work:
FutureBuilder(
future: fetchComic(i),
builder: (context, comicResult) => comicResult.hasData && comicResult.data != null
- ? ComicTile(comic: comicResult.data)
+ ? ComicTile(comic: comicResult.data!)
: const Center(...),
),
This satisfies the type error.
Null-safe mocking
The book recommends creating mocks for classes by extending Mockito’s Mock
class:
class MockHTTPClient extends Mock implements http.Client {}
However, if you do so, you will get errors related to nullability, such as type 'Null' is not a subtype of type 'Future<String>'
. Why is this? There is a good explanation of problems with typical mocking in Mockito’s null safety README, but in short: returning null
is a good default behavior for mocks when you aren’t using null safety, but under null safety you can’t return null
for methods that don’t have nullable return types.
To work under null safety, Mockito takes a different approach: code generation. Luckily, it doesn’t take a lot of work to get it running for the needs of the book.
Replace the Mock
subclass declaration with a use of the @GenerateNiceMocks
annotation. Note that I add a mock for File
as well since later tests use a file mock:
+import 'package:mockito/annotations.dart';
-class MockHTTPClient extends Mock implements http.Client {}
+@GenerateNiceMocks([MockSpec<http.Client>(), MockSpec<File>()])
+import `unit_test.mocks.dart';
The file we imported, unit_test.mocks.dart'
, doesn’t exist yet, but it will once we generate the code.
To generate it, we will need to add a dependency on build_runner
in pubspec.yaml
:
dev_dependencies:
# ...
build_runner: ^2.4.8
Download that dependency, which might happen automatically in your IDE; otherwise run flutter pub get
to get it.
Now, to generate the mock code, run dart run build_runner build
. When the process completes, you should see a new file unit_test.mocks.dart
alongside your unit_test.dart
file.
These mocks should be usable with the same when(…).thenAnswer(…)
API used by the mocks in the book. For example, here’s my test of the fetchComic()
function. (Note that it has a few other incidental changes from what’s in the book, including that in this mocking approach it seemed like only a few methods needed mocked return values.)
test("SelectionPage fetchComic", () {
var mockHttp = MockClient();
var latestComicFile = MockFile();
when(latestComicFile.exists()).thenAnswer((_) => Future.value(false));
when(mockHttp.read(Uri.parse("https://xkcd.com/2/info.0.json")))
.thenAnswer((_) => Future.value(comics[1]));
var selPage = SelectionPage();
expect(
await selPage.fetchComic(
"2",
httpClient: mockHttp,
comicFile: latestComicFile,
),
json.decode(comics[1]),
);
});
Cached images cleared between restarts on iOS
This last point isn’t a Dart or Flutter language issue, but rather an iOS runtime issue.
In the XKCD exercise, at one point you update the code to cache the JSON data for comics locally. That works fine. However, as a next step you also cache the image files locally, updating the JSON to point to local image paths instead of remote URLs. I ran into trouble doing this.
Caching image files works fine for me on Android (Android Studio 2023.1.1 patch 2, running an emulator with Android 11.0 (“R”)). It also runs fine on iOS the first time I boot up the app as well. But when I stop and restart the app, the images aren’t found (Xcode 15.3, running a simulator with iOS 17.2).
The directory we cache everything in is retrieved with the function getTemporaryDirectory()
. The docs for getTemporaryDirectory()
say that the directory can be cleared at any time. That seems to be what’s happening; iOS is aggressively clearing out these images. The problem is that, the way the app is coded, if the JSON cache is retained but the images are cleared, then the image won’t be present to show if offline.
The app could be updated to handle this case to re-retrieve the image if online, or to use a different directory to prevent the images from being cleared, but those are nontrivial changes. Instead I opted just to skip the image caching functionality when I was going through the exercises.
Flutter on
If you’re already reading Programming Flutter I hope these tips help unblock you. And if you haven’t read it yet (maybe not sure if a book from 2020 would still work), I hope these tips give you the confidence that it’s a great idea to pick up today!
If you’re a more experienced Flutter developer, do you have any extra insight about or suggestions for how to handle the errors I mentioned above?
If so, comment in the thread in our Discourse community or send us an email to let us know, and we’d be happy to share them out!