Challenge: A Flutter Website

I recreated my old website in Flutter and learned some things on the way

My website is almost as simple as it gets, having just 2 "pages": home and contact.

I quoted pages because there the website is actually a SPA with 2 views. I built it a couple of years ago and used PicoCMS and its out-of-the-box Astral theme for that.

It is more like a business card where people can see the services provided by the Constanting sole proprietorship and they can get in contact if they require my services.

The website is responsive having a layout for mobile devices and one for the rest.

I'll provide the project code at the end as a Git repository, though I won't go into detail on all nuts and bolts that make up the website. I'll detail only what I considered challenging in the process.

Constanting's website. Constanting is a sole proprietorship that provides IT consulting and software development services.
Constanting’s website

So let's get started.

1st challenge was to recreate the same background with its overlaying repeating pattern.

In the old website, the background is composed of a 1400x900 px gradient JPG image and a transparent pixelated 128x128 px PNG image.

A pixelated background.
The pixelated background.

The background.jpg gradient image stretches to cover the width and height of the entire browser window and the overlay.png transparent pixelated images overlay it using horizontal and vertical repetition.

To obtain this effect in Flutter one solution is:

home: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/background.jpg"),
fit: BoxFit.fill
),
),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/overlay.png"),
repeat: ImageRepeat.repeat,
),
),
child: HomePage(title: 'CONSTANTING'),
),
),

I previously embedded the above-referenced assets in the pubspec.yaml file, like this:

# To add assets to your application, add an assets section, like this:
assets:
- assets/images/background.jpg
- assets/images/overlay.jpg

In the root folder there is an assets folder and within it an images one that contains the 2 images.

The same overlay effect was achieved on Constanting's logo using the same technique.

2nd challenge was to use the same icon buttons like the ones I have on the old website.

The custom logo icon.
The custom logo icon.

One is a custom font icon that I created with Constanting's logo and the other is a Font Awesome icon of an envelope.

For the first one, I created a new version of the font icon specific for Flutter using the https://www.fluttericon.com/ website.

All you need to create a usable icon is the icon's SVG file. Drag and drop, and after you select the icon, you are ready to download your custom font icon(s). Note that you might get some warnings related to your SVG, although as long as the preview looks fine in the web app you're good to go.

For the second one, I could've used the pub.dev font_awesome_flutter package to use the exact icon.

dependencies:
font_awesome_flutter: ^8.8.1

It makes little sense to add as a dependency a package with almost 8000 icons just to use one of them.

Instead, because it was available on the fluttericon.com web app, I chose to include it in the custom font I just created.

I named this font ConstantingIcons.

fonts:
- family: ConstantingIcons
fonts:
- asset: assets/fonts/ConstantingIcons.ttf

3rd challenge was to use the spread operator in a ternary conditional expression.

Though this is more a language level issue that I found, I think it worth making a notice of it.

Due to the responsive nature of the website, I want to add specific widgets when the window's width size is within certain brackets. That means to insert widgets dynamically in a Row or Column.

For example, we have the following customListOfWidgets:

List<Widgets> get customListOfWidgets {
return [
Widget1(),
Widget2(),
Widget3(),
]
}

If we were to feed it as children to a Column it'd be quite simple:

Column(
children: customListOfWidgets,
),

In the case, we want to intercalate the list of widgets between other children, the following use of (the spread operator) would work:

Column(
children: <Widget>[
Header(),
...customListOfWidgets,
Footer(),
],
),

Spread unravels the List<T> into single T typed elements. In our case from a list of widgets (List<Widget>), we get the individual Widgets as we need them for placing them between the existing children of the Column.

If the case we want to intercalate conditionally my first option to do that was:

Column(
children: <Widget>[
Header(),
condition ? ...customListOfWidgets : OtherWidget(),
Footer(),
],
),

It seems the above won't compile and the following alternates would work just fine:

Column(
children: <Widget>[
Header(),
...(condition ? customListOfWidgets : [OtherWidget()]),
Footer(),
],
),

Or:

Column(
children: <Widget>[
Header(),
if(condition)
...customListOfWidgets
else
OtherWidget(),
Footer(),
],
),

In the context of responsiveness, the condition is usually set around MediaQuery.of(context).size's width and/or height.

4th challenge was to match the contact form styling and spacing of the old website.

The non-mobile version of the website.
The non-mobile version of the website.

For this, I created a WebsiteColors class where I stored most of the colors used within the original website:

class WebsiteColors {
static const white = const Color(0xaaffffff);
static const darkGrey = const Color(0xff363636);
static const grey = const Color(0xff777777);
static const lightGrey = const Color(0xffaaaaaa);
static const black = const Color(0xff222222);
static const greyBackground = const Color(0xfffafafa);
static const formBackground = const Color(0xfff4f4f4);
}

These were gathered using the Inspect feature from Chrome. Font style and padding measurements were taken using the same method.

For customizing the TextFormFields I used the global ThemeData and setup the InputDecorationTheme:

InputDecorationTheme(
hoverColor: WebsiteColors.formBackground,
fillColor: WebsiteColors.formBackground,
focusColor: WebsiteColors.white,
filled: true,
border: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue[800], width: 2.0),
),
hintStyle: TextStyle( color: WebsiteColors.lightGrey, fontSize: 13.0)
)

Wherever I needed some spacing between the website’s widgets I used the SizedBox and its height property: SizedBox(height: 10.0),.

5th challenge, and last I took with this exercise, was to recreate the contact form functionality.

For this, I used the Form widget, some TextEditingControllers, and FocusNodes.

I’ve set a FormState key named _formKey to the Forms. This key allows us to work with the form items. As you’ll see below I only used it to reset the form after the Send button was pressed. Note that in code we have twice the elements of the website. Once for the mobile version and once for the non-mobile one.

Then I created a contact() function as follows:

import 'package:universal_html/html.dart' as html;void contact() {
List target = [name, email, subject, message];
List targetFocus = [nameFocus, emailFocus, subjectFocus, messageFocus];
for(int i=0; i<target.length; i++) {
if(target[i].text.isEmpty) {
FocusScope.of(context).requestFocus(targetFocus[i]);
return;
}
}
html.window.open('mailto:a.flutter.dev@gmail.com?subject=${subject.text}&body=${message.text}&cc=${name.text}<${email.text}>', '_self');
_formKey.currentState.reset();
}

The above function first makes sure all input fields are not empty. If any is empty it sets the focus on that one and waits for user input. This could be done using the built-in form validation methods.

Then it uses the html package to open a mailto link. If you want to learn more about specialized links, you can do that here. This link allows us to open an email client with all fields pre-filled with data that we captured within our form.

The original implementation sends the form data on a server-side script that sends the email, though for the sake of simplicity I’ll use a local email client in this exercise.

The last step is to clean the form fields using the reset() method on the currentState of the form’s key.

Conclusion

There are still plenty of things that can be done in order to have an exact match of the original website. Most of these are animations or small adjustments for a pixel-perfect match.

Also, there are plenty of paths to get the same functionality or effects, and I explored just some.

For now, I’m happy with the results and I resolved the needs and wants that I had regarding this exercise.

You can see the Flutter website here, and the original website here. The code for this project is in this GitHub repository.

If you think I missed something or have a challenge yourself, please let me know and I’ll do my best to give you a hint or resolve it.

Tha(nk|t’)s all!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store