Categories
dart flutter software_design software_dev

A solid architecture is the bedrock of your app

— WIP — but publishing forces me to come back and continue!

If you can nail a good architecture it makes all sorts of things possible, whereas a bad architecture can weigh like an anchor on your development.

In the last post on software architecture, I showed a solution that I would only describe as lack of architecture. In this post I plan on laying the groundwork for a solid architecture to the WeatherSdbx app. Architecture can also be referred to as software design and describes the inner workings of the app. I’ve found that users/shareholders are usually more concerned about the UI design of an app, which is certainly important, but a well thought out architecture can help the development, testing and deployment move much more smoothly.

The sketch above captures a couple ideas (along with a doodle or three) for the WeatherSdbx architecture and a general one I use for most of my Flutter apps. The central component is the AppMgr which connects the UI to the app components and contains the business logic to run the application.

The components of ver0.1.0 are as follows:

  • main app to configure setup
  • home screen
  • current observation widget
  • app mgr

The goal of V0.1.0 is to setup a structure which can be expanded on in future versions with more functionality such as storage, http communication and so forth. One of the goals is to have a structure that functional components can be added to separately, maybe even just stubbed out, to make the entire app easier to build and test.

Categories
dart flutter software_dev

Dart extension experiment

I had a use for Dart extensions, but after playing with the code, I found I could make it more compact with an existing method.

I work with an app which utilizes colors at 10% opacity. This sounds like a great place to use an extension on the Color object.

extension ColorOpacity on Color {
  Color get opacity10 => withOpacity(0.1);
}

This works great…and I can’t help play around with the idea. Why not multiple opacity entries to show off the range of a given color.

const Color colorCoolBlue = Color(0xFF3BB2E2);

extension ColorOpacity on Color {
  Color get opacity75 => withOpacity(0.75);
  Color get opacity50 => withOpacity(0.5);
  Color get opacity25 => withOpacity(0.25);
  Color get opacity10 => withOpacity(0.1);
}

With a simple widget to show off the results.

import 'package:flutter/material.dart';
import 'package:weathersdbx/ui/ui_const.dart';

///
/// Simple screen to try out some color ideas
///
class ExpColorsWidget extends StatelessWidget {
  const ExpColorsWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget _constructColorSlice(Color color) => Expanded(
          child: Container(
            color: color,
          ),
        );
    Widget _constructBody() {
      return SizedBox(
        height: 200,
        child: Row(
          children: [
            _constructColorSlice(colorCoolBlue),
            _constructColorSlice(colorCoolBlue.opacity75),
            _constructColorSlice(colorCoolBlue.opacity50),
            _constructColorSlice(colorCoolBlue.opacity25),
            _constructColorSlice(colorCoolBlue.opacity10),
          ],
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text("Colors Experiment"),
      ),
      body: _constructBody(),
    );
  }
}

Have to admit I was a bit challenged to get all the rows to have the same width without hardcoding until I found this answer on StackOverflow thanks to diegoveloper about using the Expanded widget.

The method constructBody can further be refactored to move the color row to a separate method.

Widget _constructColorRow(
  Color color, {
  double height = 200,
}) =>
    SizedBox(
      height: height,
      child: Row(
        children: [
          _constructColorSlice(color),
          _constructColorSlice(color.opacity75),
          _constructColorSlice(color.opacity50),
          _constructColorSlice(color.opacity25),
          _constructColorSlice(color.opacity10),
        ],
      ),
    );

Widget _constructBody() {
  return _constructColorRow(colorCoolBlue);
}

And if feels like the next progression of this little experiment is to allow multiple colors, and, if we can expand one way…why not both?

const colorCoolBlue = Color(0xFF3BB2E2);
const colorDeepRed = Color(0xFF940000);

Widget _constructColorRow(
  Color color) =>
    Expanded(
      child: Row(
        children: [
          _constructColorSlice(color),
          _constructColorSlice(color.opacity75),
          _constructColorSlice(color.opacity50),
          _constructColorSlice(color.opacity25),
          _constructColorSlice(color.opacity10),
        ],
      ),
    );


Widget _constructBody() => Column(
      children: [
        _constructColorRow(colorCoolBlue),
        _constructColorRow(colorDeepRed),
      ],
    );

Now the opacity extension feels restricting, the following code goes back to the Color.withOpacity method to lift this restriction.

const defaultOpacityList = [1.0, 0.75, 0.5, 0.25, 0.10];

Widget _constructColorRow(Color color,
        {opacityList = defaultOpacityList}) =>
    Expanded(
      child: Row(children: [
        ...opacityList
            .map(
              (opacity) => _constructColorSlice(color.withOpacity(opacity)),
            )
            .toList()
      ]),
    );

Finally, adding a Dart data class to define the color and its opacities, the code has even more versatility. A further step could easily take the data out of the code altogether into something like JSON for further functionality.

class ColorDef {
  final Color baseColor;
  final List<double> opacityList;

  ColorDef(this.baseColor, this.opacityList);
}

Full final screen, exp_colors_widget.dart, can be found in my blogsdbx repository.

Categories
dart flutter software_dev testing

Wrap widgets in MaterialApp and Scaffold for Widget Testing

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);
  });
}
Categories
dart flutter one_liner software_dev

Convert TimeOfDay to DateTime

Simple convenience method to change a Flutter TimeOfDay object to a Dart DateTime object.

DateTime _timeOfDayToDateTime(
  TimeOfDay time, {
  int year = 1970,
  int month = 1,
  int day = 1,
}) =>
    DateTime(
      year,
      month,
      day,
      time.hour,
      time.minute,
    );
Categories
one_liner software_dev

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]"

Categories
dart flutter software_design software_dev

WeatherSdbx Intro, Bad Architecture Example

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.

  1. Can’t reuse http code in other UI widgets
  2. Quickly modifying UI is inhibited because calls to the backend are always made.
  3. If backend goes down or dev machine is offline UI will stop working.
  4. If backend calls cost money, changes to UI are expensive.
  5. If backend changes or another service is used, every http call in widgets need to be changed.
  6. Testing is very hard if not impossible.
  7. Data parsing isn’t shared.
  8. If data format changes, it’ll have to be changed in multiple widgets.
  9. Error handling is hard and not shared.
  10. Location is hard coded and not shared with other widgets.
  11. 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.

Categories
software_dev

HelloWorld

return Scaffold(
  appBar: AppBar(
    title: const Text("QuestingInc"),
  ),
  body: const Text(
    "Hello World",
    style: TextStyle(
      fontSize: 36,
      fontWeight: FontWeight.bold,
    ),
  ),
);