borntofrappe / flutter-beginners-tutorial

Code and notes following the [Flutter Beginners playlist](https://youtube.com/playlist?list=PL4cUxeGkcC9jLYyp2Aoh6hcWuxFDX6PBJ) on The Net Ninja YouTube channel.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

the notes do not consider the installation of Flutter, Dart or again Android Studio, since the instructions are time-sensitive and version-specific

primer.dart

dartpad.dev provides a playground for Dart code

A .dart script includes one essential function in main.

void main() {
	print('Hello');
}

Once you execute the program the script follows the instructions in the body of the function.

Variables

Dart supports static and dynamic typing.

Static: define the type of the variable before its name.

String name = 'Eliza';
int age = 27;
bool isLoggedIn = false;

You can change the value of the variable, but only mantaining the type.

name = 42;
// A value of type 'int' can't be assigned to a variable of type 'String'.

Dynamic: add the dynamic keyword before the name of the variable

dynamic name = 'Eliza';
name = 42;

You can change the value and also the type of the variable.

Functions

main provides a first example of a function.

void main() {

}

void describes the value returned by the function — in this instance nothing.

Create functions specifying the return type and the function's name.

String getName() {
  	return 'Eliza';
}

Call the function by name.

void main() {
  	print(getName());
}

Lists

A variable stores a single piece of data.

A list keeps track of multiple values.

List numbers = [3, 12, 1];

Access values by index starting at 0.

print(numbers[1]);
// 12

Add values with the add method`.

numbers.add(7);

Remove element with the remove method, pointing to the desired value.

numbers.remove(3);

The function only deletes the first instance.

List numbers = [3, 12, 1, 3, 2];
numbers.remove(3);
print(numbers);
// [12, 1, 3, 2]

If there is no value the list is not modified — and no warning is raised.

List numbers = [3, 12, 1];
numbers.remove(0);
print(numbers);
// [3, 12, 1]

When you initialize the collection with List the collection is able to store different types at the same time.

numbers.add('Eliza');

Add a type in between closing — < — and opening — > — tags to guarantee a specific type.

List<int> numbers = [3, 12, 1];
numbers.add('Eliza');
// The argument type 'String' can't be assigned to the parameter type 'int'.

Classes

Classes work as blueprints for objects, entities with properties and methods.

Define a class.

class User {
    String username = 'Eliza';
    int age = 27;
    bool isLoggedIn = false;

    void login() {
        isLoggedIn = true;
        print('User is logged in');
    }
}

Create an instance using the name of the class as its type.

User eliza = User();
print(eliza.age); // 27

Define a constructor to create objects with a set of input values.

class User {
    String username;
    int age;
    bool isLoggedIn = false;

    User(this.username, this.age, this.isLoggedIn);
}

Pass the values in the instance of the class.

User user = User('Timothy', 28);
print(user.username); // Timothy
print(user.age); // 28

dart.dev suggests using initializing formals over the instruction described in the course.

/*
User(String username, int age, bool isLoggedIn) {
    this.username = username;
    this.age = age;
    this.isLoggedIn = isLoggedIn;
}

*/

User(this.username, this.age, this.isLoggedIn);

Inheritance

A class is able to extend another entity.

class PoliteUser extends User {
  PoliteUser(String username, int age, bool isLoggedIn): super(username, age, isLoggedIn);

  void greet() {
    print('Jolly greetings to you');
  }
}

super works to have the class initialize the arguments required by the parent class.

PoliteUser politeUser = PoliteUser('Grace', 28, false);
politeUser.greet();

The extended object is equipped with its properties and methods while retaining the corresponding values from the parent class.

hello_world

Widgets

Flutter is centered on the notion of widgets.

A trivial example comes in the form of an application with a root widget nesting two widget for an app bar and a container. The app bar might then nest a text widget, while the container might include an image widget.

Each widget has its own set of properties to customize its appearance and logic. For instance, textAlign modifies the alignment of text, elevation updates the vertical priority of a button.

Widgets are implemented with classes in the Dart programming language.

Starter project

Create a project in one of two ways:

  • with Android Studio and the Flutter plugin

  • with Visual Studio Code and the Flutter and Dart extensions

The project houses the dart script responsible for the application in the lib folder.

Past the import statement the main function returns a widget.

import 'package:flutter/material.dart';

void main() {
    runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    /* ... */
}

The class is a widget which structures the application with a widget tree.

To get started remove all the code but the logic starting the application.

void main() {
  runApp(
      // ...
  );
}

In runApp add a MaterialApp widget, a wrapper to benefit from the material guidelines.

MaterialApp(
)

In the widget describe properties in a comma separated list.

 MaterialApp(
    home: Text('Hello world'),
)

With home the widget renders an additional widget — Text — with an arbitrary string.

Scaffold and AppBar

With a scaffold widget describe the layout of the application.

home: Scaffold(
    appBar: AppBar(
        title: Text('Hello World'),
    ),
)

AppBar adds a bar at the top of the screen. The widget accepts a title property to describe a specific string. Notice that the title is included with yet another widget — Text.

The tree is built in this fashion nesting properties and values.

appBar: AppBar(
    title: Text('Hello World'),
    centerTitle: true,
),

Beside the bar add text in the application with the body property.

appBar: AppBar(),
body: Text('It works!')

Use the Center widget to center the text vertically and horizontally.

body: Center(
    child: Text(),
)

Notice the widget is included through the child field.

Use a FloatingActionButton widget to add a button — by default in the bottom right corner.

body: Center(),
floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Text('Click'),
),

The button requires a onPressed field — in this instance an empty anonymous function.

Explore the flutter API for all properties and supported values.

Colors and fonts

Change the appearance of the widget with properties such as backgroundColor.

appBar: AppBar(
    backgroundColor: Colors.green,
)

The color is included through the material API, and it is possible to choose from a specific strength.

backgroundColor: Colors.green,
+backgroundColor: Colors.green[500],

The property is also available for the button widget.

FloatingActionButton(
    backgroundColor: Colors.green[500],
)

For the text widget update the appearance with the style field and a TextStyle widget.

child: Text(
    'It works!',
    style: TextStyle(
        fontSize: 20.0,
        letterSpacing: 2.0,
        color: Colors.grey[700]
    ),
)

To add a custom font create a new repository fonts and add the .ttf file.

Open pubspec.yaml and update the configuration.

flutter:
  fonts:
    - family: Hubballi
      fonts:
        - asset: fonts/Hubballi-Regular.ttf

YAML is based on indentation, with each nested field set two spaces from the parent node.


Refer to the font by name in the TextStyle widget.

style: TextStyle(
    fontFamily: 'Hubballi',
    // ...
),

Stateless widget and hot reload

At the bottom of the script create a custom widget.

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold();
  }
}

In this instance you extend the stateless widget, meaning the state of the widget doesn't change over time.

In the build function return the widget tree described in the home field.

return Scaffold(
    appBar: AppBar(
        title: Text('Hello World'),
        // ..
    ),
    // ..
);

In the main function rely on the class instead of the widget tree.

MaterialApp(
    home: Home(),
)

This setup enables hot reloading. Whenever you update the widget tree the change is reflected in the device preview.

Images and assets

Refer to images with a URL or a relative path.

Over the network use the network image widget.

body: Center(
    child: Image(
        image: NetworkImage('https://images.pexels.com/photos/1517358/pexels-photo-1517358.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'),
    ),
),

From a local file use the asset image widget.

body: Center(
    child: Image(
        image: AssetImage('assets/rose.jpeg'),
    ),
),

Similarly to font files add the folder and the file in the .yaml config file.

flutter:
  assets:
    - assets/rose.jpeg

Point to the folder to consider all available images.

flutter:
  assets:
    - assets/

The material API offers two alternatives to include images with Image.network and Image.asset.

child: Image.network('https://images.pexels.com/photos/1517358/pexels-photo-1517358.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'),

child: Image.asset('assets/rose.jpeg'),

Buttons and icons

Add icons with an icon widget, picking from one of the icons in the material library.

child: Icon(
    Icons.add_location,
    color: Colors.blue,
    size: 50.0,
),

Add a button with a button widget. The material API offers different styles, for instance a filled button with a shadow in ElevatedButton.

ElevatedButton(
    onPressed: () {},
    child: Text('Click me'),
),

A button without a solid background with TextButton.

TextButton(
    onPressed: () {},
    child: Text('Click me'),
),

With onPressed the widget reacts to a click.

onPressed: () {
    print('Button clicked');
},

In this instance the message is logged in the console — refer to the "Run" tab for Android Studio, the "Debug Console" tab for Visual Studio Code.

For the icon append the .icon keyword to the widget. Refer to the specific icon in the icon field and an icon widget.

TextButton.icon(
    onPressed: () {
        print('Button clicked');
    },
    icon: Icon(Icons.email),
    label: Text('Mail me'),
),

For a button with only an ico use the icon button widget.

IconButton(
    onPressed: () {},
    icon: Icon(Icons.email),
),

Layout

Container

Use the container widget to wrap around other widgets and add properties like padding and margin. Nest a single widget with the child property.

body: Container(
    child: Text(
        'Hello world',
        style: TextStyle(
            fontSize: 28.0,
        ),
    ),
),

The color property affects the background of the container.

Container(
    color: Colors.grey[300],
)

Without a child widget the container expands to cover the available space.

With a child the container limits itself to the width and height necessary for the widget.

For spacing the padding and margin properties rely on an inset object.

padding: EdgeInsets.all(20.0),

EdgeInsets changes the spacing on all sides, but also the top/bottom, left/right sections or again the four sides individually — left, top, right, bottom.

padding: EdgeInsets.symmetric(vertical: 20.0),
padding: EdgeInsets.fromLTRB(10.0, 10.0, 5.0, 5.0),

The same inset is available for the margin property, adding whitespace around the widget.

To add padding to a single object an alternative is to wrap the widget in a padding widget.

Padding(
    padding: EdgeInsets.all(10.0),
    child: Text('Hello'),
)

Rows

Use the rows widget to display multiple widgets in the same row. In this instance add the widget in a children property.

Row(
    children: <Widget>[
        Text(),
        TextButton.icon(),
        Padding(
            child: Text(),
        ),
    ],
),

Align the widgets horizontally and vertically with the mainAxisAlignment and crossAxisAlignment properties.

mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: // ...,

Columns

Use the column widget to display multiple widget in the same column.

Column(
    children: <Widget>[],
),

The main and cross axis are opposite to the row.

Expanded

Use the expanded widget to have a widget expand to the available space.

Expanded(
    child: TextButton.icon(),
),

With multiple expanded widget specify the portion of the available space with the flex property.

Expanded(
    flex: 1,
    child: Text('Hello world')
),
Expanded(
    flex: 2,
    child: ElevatedButton(
        onPressed: () {},
        child: Text('Click me')
    )
),

id_card

Create a dummy app to display static values in an arbitrary widget tree.

Past the AppBar widget the body property includes the following widget tree.

Padding
	Column
		Center
			CircleAvatar
		Divider
		Text
		SizedBox
		Text
		SizedBox
		Text
		SizedBox
		Text
		SizedBox
		Row
			Icon
			SizedBox
			Text

In terms of new widgets:

  • use CircleAvatar to add a circle and the background property to include an image — here with an asset widget

    CircleAvatar(
    	backgroundImage: AssetImage('assets/bingdwendwen.jpg'),
    	radius: 56.0,
    ),
  • use Divider to separate the content — here with an arbitrary vertical gap

    Divider(
    	height: 56.0,
    	color: Colors.grey[500],
    ),
  • use SizedBox to add whitespace between widgets — in the column size the box in its height, in the row size the box in terms of width

    SizedBox(height: 8.0),
    SizedBox(width: 8.0),

Be sure to update pubspec.yaml to make the local image available.

flutter
  assets:
    - assets/

id_card_stateful

Create a new project app using id_card as a starting point.

Stateful widgets

Instead of relying on a stateless widget create a stateful widget to consider data which changes over time.

The goal is to ultimately update the application to reflect the change in state.

Similarly to the stateless widget define a class which extends the built-in StatefulWidget.

class Home extends StatefulWidget {
	@override
    _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
	@override
    Widget build(BuildContext context) {
		return Container();
	}
}

There are two classes since the idea is to link a state object — _HomeState — to a widget — Home.

Define the variable at the top of the class extending the state.

class _HomeState extends State<Home> {
  int hearts = 0;

	// @override...
}

Include the value with the $ prefix.

Text(
	'$hearts'
)

Update the state, for instance at the press of a button, in the body of the setState function.

onPressed: () {
	setState(() {
		hearts += 1;
	});
}

Through setState Flutter knows to rebuild the widget tree and update the relevant section.

quotes_list

List of data

As a setup create a stateful widget.

class QuoteList extends StatefulWidget {
    @override
    _QuoteListState createState() => _QuoteListState();
}

class _QuoteListState extends State<QuoteList> {
    @override
    Widget build(BuildContext context) {
        return Scaffold();
    }
}

Use the widget in the home property of the MaterialApp instance.

void main() => runApp(
    MaterialApp(
        home: QuoteList()
    ),
);

In terms of widget tree use a scaffold widget with an arbitrary background color, application bar and body.

Scaffold(
    backgroundColor: Colors.grey[100],
    appBar: AppBar(
        title: Text('Quotes List'),
        centerTitle: true,
        backgroundColor: Colors.red[900],
    ),
    body: Column(
        children: []
    ),
);

In the column the idea is to include one widget for each quote from in a list.

Create a list of strings at the top of the widget.

List<String> quotes = [
    'I don\'t wanna stop at all',
    'You can\'t start a fire without a spark',
    'Stop when the red lights flash',
];

To render each and every string loop through the list with the map function.

children: quotes.map((quote) {

}),

In the body return a text widget with the value of the quote

children: quotes.map((quote) {
    return Text(quote);
}),

Since children expects a <List>, and not an iterable of <Text> nodes, chain the toList() method.

children: quotes.map((quote) {
    return Text(quote);
}).toList(),

The function can be made into an arrow function as a matter of preference.

quotes.map((quote) => Text(quote)).toList()

Custom classes

Instead of adding a quote as-is, create a separate dart script to encapsulate the widget and its logic — quote.dart.

class Quote {
    String text;
    String author;

    Quote(this.text, this.author);
}

In the main script import the class.

import 'quote.dart';

With this starting point you'd instantiate a new quote passing the text and author in order.

Quote quote = Quote('Third time\'s the charm', 'me')

As a matter of preference, however, the course introduces named parameters.

Update the constructor.

Quote(this.text, this.author);
+Quote({ this.text, this.author });

Dart asks to initialize the variables to a non-null value or add the required keyword.

Quote({ this.text, this.author });
+Quote({ this.text = '', this.author = '' });
+Quote({ required this.text, required this.author });

Pass the text and author specifying the value after the corresponding keyword. In this instance the order of the arguments doesn't matter.

Quote quote = Quote(text: 'Third time\'s the charm', author: 'me')

Update quotes to define a list of quote objects instead of stings.

List<Quote> quotes = [
    Quote(),
    Quote(),
]

Loop through the quotes and add the relevant data in quotes, wrapping the property after the dollar sign in curly braces.

Text('${quote.text} - ${quote.author}')

Cards

Instead of adding the quotes in a text widget the idea is to create a more complex widget, a card displaying the information in a column.

Create a separate function to return the widget tree.

Widget quoteTemplate(Quote quote) {
  return Card(
    margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 24.0),
    child: Column(
      children: <Widget>[
        Text(
          quote.text,
          style: TextStyle(
            fontSize: 14.0,
            color: Colors.grey[600],
          ),
        ),
        SizedBox(height: 6.0),
        Text(
          quote.author,
          style: TextStyle(
            fontSize: 18.0,
            color: Colors.grey[700],
          ),
        ),
      ]
    ),
  );
}

Use the function instead of the text node.

-Text('${quote.text} - ${quote.author}')
+quoteTempalte(quote))

Ultimately the widget tree is updated to:

  • add padding for each card

  • have the content of each card stretch to cover the available width


Extracting widgets

Instead of relying on a function the idea is to extract the widgets in a stateless widget. This is to ultimately provide the relevant structure creating an instance of the class.

-return Card()
+return new QuoteCard(quote: quote);

Initialize the class as an extension of a stateless widget which returns the previous card widget.

class QuoteCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
      return Card(
          // ...
      )
  }
}

In the return statement the class relies on a quote, so that it is necessary to specify its value in the constructor.

class QuoteCard extends StatelessWidget {
    Quote quote;

    QuoteCard({required this.quote});
);

As a stateless widget, however, you need to describe the input value with the final keyword. This is data which doesn't change.

Quote quote;
+final Quote quote;

With this setup create an instance of the class in the body of the templating function

Widget quoteTemplate(Quote quote) {
  return new QuoteCard(quote: quote);
}

Ultimately it is possible to directly return the instance in the mapping function.

-quoteTempalte(quote))
+new QuoteCard(quote: quote)

The new keyword is also optional.

-new QuoteCard(quote: quote)
+QuoteCard(quote: quote)

To improve the structure of the code, extract the class in its own file and import the widget at the top of the script.

import QuoteCard from 'quote_card.dart';

In the separate file you need to import the material library as well as the quote class.

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

Functions as parameters

Add a button widget to each instance of the quote card — in this instance an icon button.

IconButton(
    onPressed: () { },
    icon: Icon(Icons.delete_outline)
),

QuoteCard is a stateless component, so that it is not able to update the state of the application. With this in mind the idea is to update the state from QuoteList by way of a function and then pass the function an argument.

Define a delete function alongside the quote field. Through setState use the function to remove the associated quote.

QuoteCard(
    quote: quote,
    delete: () {
        setState(() {
            quotes.remove(quote);
        });
    }
)

Update the card class to receive the function together with the quote.

final Function delete;

QuoteCard({ required this.quote, required this.delete });

In the button widget call the function.

IconButton(
    onPressed: () { delete(); },
    icon: Icon(Icons.delete_outline)
),

world_time

Pages and packages

The application is set to have three pages, each encapsulated in its own .dart file.

main.dart
pages
    loading.dart
    home.dart
    location.dart

Initialize the pages with stateful widgets. For instance and home.dart create the classes following the example of dynamic quotes.

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      body: Text('Home'),
    );
  }
}

In the script main.dart import the pages.

import 'pages/loading.dart';
import 'pages/home.dart';
import 'pages/location.dart';

Use the relative path or the name of the project itself with the package: prefix. This is similar to how the main script imports the material library.

import 'package:world_time/pages/home.dart';

Use a specific page in the home property of the MaterialApp widget.

void main() {
  runApp(MaterialApp(
    home: Home()
  ));
}

With home.dart the course does not include an application bar, so that the text is initially pushed at the top, potentially behind the status bar. Use the SafeArea widget to avoid this issue.

body: SafeArea(
    child: Text('Home'),
)

Maps and routing

In the dart language a map describes a data structure with key value pairs.

Map student = {
    'name': 'Timothy',
    'age': 28,
};

Extract specific values with bracket notation.

print(student['name']); // Timothy
print(student['age']); // 28

The data structure is relevant for the pages in the application since routing is based on a map in the routes field.

MaterialApp(
    routes: {}
)

In this map the key describes the path to the route. The value is a function which receives context as an argument.

routes: {
    '/': (context) {}
}

In the function return a specific widget

routes: {
    '/': (context) => Loading(),
    '/home': (context) => Home(),
    '/location': (context) => Location(),
}

The home field creates a conflict with routes. Remove the first property to rely on the routes instead.

-home: Home(),
routes: {

By default flutter loads the widget described in /. Provide an alternative in the initialRoute field.

initialRoute: '/home',
routes: {}

To navigate between routes use the Navigator object.

Add a button in the home widget.

At the press of a button, for instance, move to the location screen with the pushNamed method.

onPressed: () {
    Navigator.pushNamed(context, '/location');
}

With pushNamed you push the widget on top of the existing one. In the moment you add an app bar in the new page flutter adds a back button to navigate back to the previous widget.

Scaffold(
    backgroundColor: Colors.grey[100],
    appBar: AppBar(
        title: Text('Change location'),
        centerTitle: true,
    ),
    body: Text('Location'),
)

Widget lifecycle

Stateful widgets register events for specific moments in their lifecycle.

One of these events is already considered with the build function.

class _HomeState extends State<Home> {
    @override
    Widget build(BuildContext context) {
        return SafeArea();
    }
}

The "build" event is registered every time the widget is built — when initialized and every time the script calls the setState function.

The "initState" event is registered once when the widget is first initialized. You tap into this event with the initState function.

@override
void initState() {
    super.initState();

    print('Init');
}

Call the function on super to trigger the original function.

The "dispose" event is registered once when the widget is finally destroyed.

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

    print('Dispose');
}

Asynchorous code

To implement asynchronous code — logic which takes time and resolves at a point in time without blocking the application — dart provides the async and await keywords alongside the concept of futures.

To simulate the feature define a function getData.

void getData() {

}

Call the function in the build method of the home widget, or again the initState method of the location screen.

@override
Widget build(BuildContext context) {
    getData();
}

In getData use Future.delayed to wait for a specific amount of time.

Future.delayed(Duration(seconds: 3), () {
    print('Hello delayed');
});

The Duration global specifies a duration with a specific time.

In the snippet the print statement is run after 3 seconds. In those seconds, however, the application continues.

Future.delayed(Duration(seconds: 3), () {
    print('Hello delayed');
});
print('Hello now');

In the moment you call the function you'd see the string "Hello now" immediately, "Hello delayed" after the specified lapse.

If you want to wait for the execution of a future make the function into an async function.

void getData() async {

}

Use the await keyword to wait for the completion of the future.

await Future.delayed(Duration(seconds: 3), () {
    print('Hello delayed')
});
print('Hello now?');

In the snippet the order of the print statement would be the opposite of the previous example.

Store a specific value from the future by returning something in the body of the curly braces.

String username = await Future.delayed(Duration(seconds: 3), () {
    return 'Timothy';
});
print(username);

Flutter packages

pub.dev is the relevant package repository for dart and flutter applications. Here you find modules and functionalities developed by other developers to achieve specific features.

For the world time app one of the useful packages is http. The module handles network requests to specific endpoints.

Install the package adding a version to pubspec.yaml and the dependencies field.

dependencies:
  http: ^0.13.4

Import the module in the benefiting files.

import 'package:http/http.dart';

Get sample data

Update the application to launch the loading widget.

-initialRoute: '/home',
routes: {}

Move the logic of getData from the home widget to the loading one, removing the existing futures and print statements.

In getData make a network request to the JSON placeholder API.

void getData() async {
    Response response = await get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
}

The response type and get functions are available from the http module. response itself provides a string in the body field.

Response response = await get();
print(response.body)
/*
{
    "userId"	1
    "id"	1
    "title"	"delectus aut autem"
    "completed"	false
}
*/

Call the function in the initState method to highlight the result in the console.

void initState() {
    super.initState();

    getData();
}

Use a converting function from the dart:convert library to turn the string into a map.

import 'dart:convert';

// in getData
Map data = jsonDecode(response.body);
print(data['title']); // delectus aut autem

Get world time

The world time API provides the time for specific time zones.

http://worldtimeapi.org/api/timezone/Europe/Rome
http://worldtimeapi.org/api/timezone/Europe/Paris

A sample request returns a JSON object with several values.

{
  "abbreviation": "CET",
  "client_ip": "80.180.189.16",
  "datetime": "2022-02-27T17:05:08.426202+01:00",
  "day_of_week": 0,
  "day_of_year": 58,
  "dst": false,
  "dst_from": null,
  "dst_offset": 0,
  "dst_until": null,
  "raw_offset": 3600,
  "timezone": "Europe/Paris",
  "unixtime": 1645977908,
  "utc_datetime": "2022-02-27T16:05:08.426202+00:00",
  "utc_offset": "+01:00",
  "week_number": 8
}

In the application the idea is to use utc_datetime alongside utc_offset to find the time at the specific location.

Rename the test getData function to getTime

-void getData() async {
+void getTime() async {
-getData()
+getTime();

In the body of the asynchronous function make a request with an arbitrary location.

Response response = await get();
Map data = jsonDecode(response.body);

From data retrieve the desired values for the time and offset.

String datetime = data['utc_datetime'];
String offset = data['utc_offset'];

The values are strings. Use the Datetime object and the parse method to turn the string for the datetime into a date object.

DateTime now = DateTime.parse(datetime);

The instance provides several helper methods to compute dates and extract useful data such as hour, minutes.

print(now); // 2022-02-28 16:28:01.504304Z
print(now.hour); // 16

To add an offset use the add method specifying a duration object.

now.add(Duration(hours: 1));

To include the value from the offset variable — a string — into the duration object — an integer — extract the number of hours and parse the value as an integer.

String offset = data['utc_offset']; // '+01:00'
String hours = offset.substring(1,3); // 01

now = now.add(Duration(hours: int.parse(hours)));

Note that add returns a new date object, and does not modify the original instance.

print(now)

WorldTime custom class

Create a dedicated class to perform the network request and set a specific time.

main.dart
pages
    loading.dart
    home.dart
    location.dart
services
    world_time.dart

In the script import the http module and the converting utility before initializing the class.

import 'package:http/http.dart';
import 'dart:convert';

Initialize the class with three values: location for the name displayed in the UI, flag for the png asset describing a flag in the upcoming assets folder, and url for the endpoint in the world time API.

class WorldTime {
    String location;
    String flag;
    String url = '';

    WorldTime({required this.location, required this.flag, required this.url });
}

An example instance would create an object as follows.

WorldTime instance = WorldTime(location: 'Paris', flag: 'france.png', url: 'Europe/Paris');

Create a getTime method to perform the network request and store the time in a fourth variable.

String time = '';

void getTime() async {}

Repeat the getTime function using the input url in the HTTP request.

void getTime() async {
    Response response  = await get ('/$url')

    // ...
}

Finally store the value in time.

time = now.toString();

With this setup:

  • create an instance

    WorldTime instance = WorldTime(location: 'Paris', flag: 'france.svg', url: 'Europe/Paris')
  • compute the time

    await instance.getTime()

    await pauses the execution until the async function has resolved.

    Note that await works only in an async function itself.

In the loading screen include the instruction in a dedicated function of the stateful widget.

void setupWordTime() async {
    // WorldTime ...
}

Call the function in the initState method so that the application retrieves the time as the widget is created.

initState() {
    super.initState()

    setupWorldTime();
}

With this setup the console highlights an error connected to the instruction calling getTime.

await worldTime.getTime();
// Error: This expression has type 'void' and can't be used

Update the definition of the function to describe the future included in its body.

void getTime() async {}
+Future<void> getTime() async {}

To test the feature create a string to display in a text widget.

String time = 'loading';

// in the build function
body: Center(
    child: Text(time)
)

Make a call to the setState function once the time is available.

await instance.getTime();
setState(() {
    time = instance.time;
})

Handling errors

Asynchronous code is resolved at a point in time. Or, raises an error in the moment the task fails. One wait to handle the occurrence is to show a default string when the async function fails.

try {
    await instance.getTime();
    setState(() {
        time = instance.time;
    })
}
catch(error) {
    print('Caught error $error');
    time = 'Could not get time data';
}

To test the error try to execute the request to a wrong URL.

Response response = await get(Uri.parse('http://worldtimeaprg/api/timezone/Europe/$url'));

// Failed host lookup: 'worldtimeaprg' (OS Error: No address associated with hostname, errno = 7)

Pass route data

Instead of displaying the time in the loading screen the idea is to pass the data to the home screen.

Remove the time reference and the set state and push the home screen.

Navigator.pushNamed(context, '/home')

pushNamed adds the home screen on top of the loading screen, but it is no longer necessary to maintain the previous widget. Use pushReplacementNamed instead.

Navigator.pushReplacementNamed(context, '/home')

Pass data through a keyword argument describing a map.

Navigator.pushReplacementNamed(context, '/home', arguments: {
    'time': instance.time
})

For the application pass the time alongside the location and flag.

{
    'location': instance.location,
    'flag': instance.flag,
    'time': instance.time,
}

In the home widget the information is made available in the build function and with a specific sequence.

Widget build(BuildContext context) {
    print(ModalRoute.of(context)?.settings.arguments);
}

Initialize a map to store the data and update the variable with the arguments field.

Map data = {};

Widget build(BuildContext context) {
    data = ModalRoute.of(context).settings.arguments as Map;
}

In the widget tree add the values through several text widgets.

Formatting and showing dates

Instead of storing the DateTime object as a string the idea is to format the instance with the intl package.

Similarly to the http module add the dependency to pubspec.yaml.

dependencies:
  intl: ^0.17.0

Import the value in the script creating the WorldTime class.

import 'package:intl/intl.dart';

Save the time not as a string but through a specific sequence to retrieve the hour and minutes

time = DateFormat.jm().format(now); // 1:20pm

Redesign widgets

Update the home widget to present the information in a column. In this column show the button to change the location, the location itself and the time.

The style of the text, in size and or color, is a matter of preference.

Loading spinner

For the loading widget the idea is to highlight the loading process with a spinner instead of a hard-coded string.

Install flutter_spinkit in pubspec.yaml.

dependencies:
  flutter_spinkit: ^5.1.0

Require the module.

import 'package:flutter_spinkit/flutter_spinkit.dart';

Replace the existing tree with a Center widget and nest as a child one of the widgets from the module:

Center(
    child: SpinKitChasingDots(
        color: Colors.grey[100],
        size: 48.0,
    )
)

Customize the spinner in terms of size and color.


Include the spinner as a child of a Scaffold widget to change the default background.

Scaffold(
    backgroundColor: Colors.indigo,
    child: SpinKitChasingDots()
)

Conditional image

In the home widget the idea is to show an image from the assets folder. One of two images depending on the time of day.

Update the yaml config file to consider the assets folder.

assets
- assets/

Update the world time class with a boolean describing the state.

bool isDayTime;

Evaluate the condition on the basis of now.

isDayTime = (now.hour > 6) & (now.hour < 20);

Pass the information in the home widget with the other arguments.

{
    'isDaytime': instance.isDayTime
}

In the home widget use the boolean to describe the path to the image.

String bgImage = data['isDaytime'] ? 'day.png' : 'night.png'

In the widget tree add a decoration field of a Container widget. Use the BoxDecoration widget to have the asset image fitted to cover the entirety of the page.

child Container(
    decoration: BoxDecoration(
        image: DecorationImage(
            image: AssetImage('asset/$bgImage'),
            fit: BoxFit.cover // full background image
        )
    )
)

Locations

In the third and final widget, location.dart, the idea is to show a list of locations to change the time displayed in the homepage.

Define a list of world time instances for a few options.

List<WorldTime> worldTimes = [
    WorldTime(location: 'Paris', flag: 'france.png', url: 'Europe/Paris'),
    WorldTime(location: 'London', flag: 'uk.png', url: 'Europe/London'),
    WorldTime(location: 'Berlin', flag: 'germany.png', url: 'Europe/Berlin'),
    WorldTime(location: 'Moscow', flag: 'russia.png', url: 'Europe/Moscow'),
];

Instead of looping through the list with the map function, like in the previous demo devoted to dynamic quotes, use a ListView.builder widget from the material library.

body: ListView.builder(
)

In the widget specify the number of items as well as a callback function receiving the context and the index of each item.

ListView.builder(
    itemCount: worldTime.length,
    itemBuilder: (context, index) {}
)

In the body of the function return a widget tree accessing the data from the individual instances of the world time class.

itemBuilder: (context, index) {
    // access locations[index]
    return Card(
        return child: ListTile()
    )
}

Use the ListTile widget to show the location after an image for the flag.

title: Text(worldTime[index].location),
leading: CircleAvatar(
    backgroundImage: AssetImage('assets/${worldTime[index].flag}')
)

Update location

From the location widget the idea is to use getTime to update the data displayed in the home widget.

In the ListTile widget add an onTap field similar to onPressed.

onTap: () {
    updateTime(index);
}

Define updateTime to consider the world time instance and set its time.

void updateTime(index) async {
    WorldTime worldTime = worldTimes[index];
    await worldTime.getTime();
}

Finally pop the screen to move back to the home widget.

Navigator.pop(context)

To pass the data back to the home screen add a map as a second argument.

Navigator.pop(context, {
    location: instance['location'],
    // ...
})

The issue in the receiving script is that as you pop the widget the home is not rebuilt. The arguments are not included as when the application moves from the loading to the home screen.

You can receive the value considering the location widget as a large future. When you push the widget on top of the home store the result of Navigator.pushNamed.

Navigator.pushNamed('/location');
+dynamic result = await Navigator.pushNamed('/location');

As you retrieve this result update the data with setState.

setState(() {
    data = {
        'time': result['time'],
        // ..
    };
});

The build function is triggered and the widget tree is rebuilt. Since the build function uses the value from the arguments field. hover, the value is not kept.

data = ModalRoute.of(context).settings.arguments;

One way to solve this issue is to use the arguments only if data is not already initialized.

data = data.isNotEmpty ? data : ModalRoute.of(context).settings.arguments

About

Code and notes following the [Flutter Beginners playlist](https://youtube.com/playlist?list=PL4cUxeGkcC9jLYyp2Aoh6hcWuxFDX6PBJ) on The Net Ninja YouTube channel.


Languages

Language:C++ 50.2%Language:CMake 23.7%Language:HTML 11.7%Language:Dart 10.5%Language:C 2.2%Language:Swift 1.2%Language:Kotlin 0.4%Language:Objective-C 0.1%