I like to doodle
Month: September 2021
When testing single widgets, wrap the widgets in a MaterialApp and Scaffold in the test to avoid the following nastiness…
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═════════════════════════
The following assertion was thrown building Text("qinc was here"):
No Directionality widget found.
RichText widgets require a Directionality widget ancestor.
The specific widget that could not find a Directionality ancestor was:
RichText
The ownership chain for the affected widget is: "RichText ← Text ← DisplayMeSomeText ← [root]"
Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the
top of your application widget tree. It determines the ambient reading direction and is used, for
example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve
EdgeInsetsDirectional, AlignmentDirectional, and other *Directional objects.
Widget under test
class DisplayMeSomeText extends StatelessWidget {
final String _theText;
const DisplayMeSomeText(this._theText, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) =>Text(_theText);
}
BAD BAD BAD
void main() {
testWidgets('Ensure expected text is displayed', (WidgetTester tester) async {
const sometext = "qinc was here";
await tester.pumpWidget(const DisplayMeSomeText(sometext));
await tester.pumpAndSettle();
expect(find.text(sometext), findsOneWidget);
});
}
BETTER BETTER BETTER
void main() {
testWidgets('Ensure expected text is displayed', (WidgetTester tester) async {
const sometext = "qinc was here";
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: DisplayMeSomeText(sometext),
),
),
);
await tester.pumpAndSettle();
expect(find.text(sometext), findsOneWidget);
});
}
To make this easier in all widget tests, the following utility method is helpful.
EVEN BETTER..imho
Widget constructTestMaterialApp(Widget widget) => MaterialApp(
home: Scaffold(
body: widget,
),
);
void main() {
testWidgets('Ensure expected text is displayed', (WidgetTester tester) async {
const sometext = "qinc was here";
await tester.pumpWidget(constructTestMaterialApp(
const DisplayMeSomeText(sometext),
));
await tester.pumpAndSettle();
expect(find.text(sometext), findsOneWidget);
});
}
Change email for git repo
Just a quick one-liner used to change the email for a git repository from the one configured on the dev machine
git config user.email "[email protected]"
Beginning a new project as a test bed to discuss some of my ideas for software development/design and specifically Flutter/Dart. WeatherSdbx is a simple weather app pulling data from the U.S. National Weather Service weather API. The code for the project is available at the following GitHub repository.
This app includes a layered software architecture to facilitate stepwise development and testing. By stepwise development I mean the ability to develop features in small steps without relying on the completeness of other components or systems. An example of this is fetching and displaying current weather observations.
Without any forethought to architecture, a call to the NWS API could be made directly from the UI to fetch the current temperature for a location.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class CurrentObsV1 extends StatefulWidget {
const CurrentObsV1({Key? key}) : super(key: key);
@override
State<CurrentObsV1> createState() => _CurrentObsV1State();
}
class _CurrentObsV1State extends State<CurrentObsV1> {
late http.Client _httpClient;
String? _temp;
@override
void initState() {
_httpClient = http.Client();
super.initState();
}
@override
Widget build(BuildContext context) {
_fetchData() async {
String stationLatest =
"https://api.weather.gov/stations/KTIW/observations/latest";
String uri = stationLatest;
final headers = {"User-Agent": "(questinginc.com, [email protected])"};
http.Response response =
await _httpClient.get(Uri.parse(uri), headers: headers);
if (response.statusCode == 200) {
Map<String, dynamic> data = jsonDecode(response.body);
setState(() {
_temp = data['properties']['temperature']['value'].toString();
});
} else {
setState(() {
_temp = "Error";
});
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("AnyTown"),
Container(height: 8,),
Text("Temp (C): ${_temp ?? ''}", style: const TextStyle(fontSize: 18),),
Container(height: 8,),
OutlinedButton(onPressed: _fetchData, child: const Text("Refresh"))
],
);
}
}
There are a number of reasons why arranging the code in this manner is a bad idea. I quickly came up with the list below and I’m sure without much thought more could be added.
- Can’t reuse http code in other UI widgets
- Quickly modifying UI is inhibited because calls to the backend are always made.
- If backend goes down or dev machine is offline UI will stop working.
- If backend calls cost money, changes to UI are expensive.
- If backend changes or another service is used, every http call in widgets need to be changed.
- Testing is very hard if not impossible.
- Data parsing isn’t shared.
- If data format changes, it’ll have to be changed in multiple widgets.
- Error handling is hard and not shared.
- Location is hard coded and not shared with other widgets.
- No data caching or logic to determine if new data should be fetched on request.
I didn’t want to spend too much time on what not to do, but did want to put something down as a foil to a better solution which will be the focus of my next architecture post.
HelloWorld
return Scaffold(
appBar: AppBar(
title: const Text("QuestingInc"),
),
body: const Text(
"Hello World",
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
),
),
);