Weather App with “flutter_bloc”

Weather App with “flutter_bloc”

Hey everyone, today we’re going to build a minimal (but functional) weather app in Flutter which demonstrates how to manage multiple blocs to implement dynamic theming, pull-to-refresh, and much more.

Hey everyone, today we’re going to build a minimal (but functional) weather app in Flutter which demonstrates how to manage multiple blocs to implement dynamic theming, pull-to-refresh, and much more.

Our weather app will pull real data from an API and demonstrate how to apply a layered architecture to separate presentational logic from business logic.

The finished product will look like this:

Let’s get started!

Setup

We’ll start off by creating a brand new Flutter project

flutter create flutter_weather

We can then go ahead and replace the contents of pubspec.yaml with:

name: flutter_weather
description: A new Flutter project.

version: 1.0.0+1

environment:
  sdk: ">=2.0.0-dev.68.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter  
  flutter_bloc: ^0.5.0
  http: ^0.12.0
  equatable: ^0.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true
  assets:
- assets/

Note: We are going to use custom assets in our project so we include the entire assets directory.

Now we can install all of our dependencies with flutter packages get.

REST API

For this application we’ll be hitting the metaweather API.

We’ll be focusing on two endpoints:

  • /api/location/search/?query=$city to get a locationId for a given city name
  • /api/location/$locationId to get the weather for a given locationId

Open https://www.metaweather.com/api/location/search/?query=london in your browser and you’ll see the following response:

[
  {
    "title": "London",
    "location_type": "City",
    "woeid": 44418,
    "latt_long": "51.506321,-0.12714"
  }
]

We can then get the where-on-earth-id (woeid) and use it to hit the location API.

Navigate to https://www.metaweather.com/api/location/44418 in your browser and you’ll see the response for weather in London. It should look something like this:

{
  "consolidated_weather": [
    {
      "id": 5565095488782336,
      "weather_state_name": "Showers",
      "weather_state_abbr": "s",
      "wind_direction_compass": "WNW",
      "created": "2019-02-10T19:55:02.434940Z",
      "applicable_date": "2019-02-10",
      "min_temp": 3.75,
      "max_temp": 6.883333333333333,
      "the_temp": 6.885,
      "wind_speed": 10.251177687940428,
      "wind_direction": 288.4087075064449,
      "air_pressure": 998.9649999999999,
      "humidity": 79,
      "visibility": 8.241867493835997,
      "predictability": 73
    },
    {
      "id": 5039805855432704,
      "weather_state_name": "Light Cloud",
      "weather_state_abbr": "lc",
      "wind_direction_compass": "NW",
      "created": "2019-02-10T19:55:02.537745Z",
      "applicable_date": "2019-02-11",
      "min_temp": 1.7699999999999998,
      "max_temp": 8.986666666666666,
      "the_temp": 8.105,
      "wind_speed": 5.198548786091227,
      "wind_direction": 319.24869874195554,
      "air_pressure": 1027.4,
      "humidity": 75,
      "visibility": 11.027785234232084,
      "predictability": 70
    },
    {
      "id": 6214207016009728,
      "weather_state_name": "Heavy Cloud",
      "weather_state_abbr": "hc",
      "wind_direction_compass": "SW",
      "created": "2019-02-10T19:55:02.736577Z",
      "applicable_date": "2019-02-12",
      "min_temp": 3.2699999999999996,
      "max_temp": 11.783333333333333,
      "the_temp": 10.425,
      "wind_speed": 6.291005350509027,
      "wind_direction": 225.7496998927606,
      "air_pressure": 1034.9099999999999,
      "humidity": 77,
      "visibility": 9.639331305177762,
      "predictability": 71
    },
    {
      "id": 6548160117735424,
      "weather_state_name": "Heavy Cloud",
      "weather_state_abbr": "hc",
      "wind_direction_compass": "SSW",
      "created": "2019-02-10T19:55:02.687267Z",
      "applicable_date": "2019-02-13",
      "min_temp": 3.526666666666667,
      "max_temp": 11.476666666666667,
      "the_temp": 10.695,
      "wind_speed": 6.524550068392587,
      "wind_direction": 203.1296143014564,
      "air_pressure": 1035.775,
      "humidity": 76,
      "visibility": 12.940987135130836,
      "predictability": 71
    },
    {
      "id": 4957149578919936,
      "weather_state_name": "Light Cloud",
      "weather_state_abbr": "lc",
      "wind_direction_compass": "SSE",
      "created": "2019-02-10T19:55:03.487370Z",
      "applicable_date": "2019-02-14",
      "min_temp": 3.4500000000000006,
      "max_temp": 12.540000000000001,
      "the_temp": 12.16,
      "wind_speed": 5.990352212916568,
      "wind_direction": 154.1901674720193,
      "air_pressure": 1035.53,
      "humidity": 71,
      "visibility": 13.873665294679075,
      "predictability": 70
    },
    {
      "id": 5277694765826048,
      "weather_state_name": "Light Cloud",
      "weather_state_abbr": "lc",
      "wind_direction_compass": "S",
      "created": "2019-02-10T19:55:04.800837Z",
      "applicable_date": "2019-02-15",
      "min_temp": 3.4,
      "max_temp": 12.986666666666666,
      "the_temp": 12.39,
      "wind_speed": 5.359238182348418,
      "wind_direction": 176.84978678797177,
      "air_pressure": 1030.96,
      "humidity": 77,
      "visibility": 9.997862483098704,
      "predictability": 70
    }
  ],
  "time": "2019-02-10T21:49:37.574260Z",
  "sun_rise": "2019-02-10T07:24:19.235049Z",
  "sun_set": "2019-02-10T17:05:51.151342Z",
  "timezone_name": "LMT",
  "parent": {
    "title": "England",
    "location_type": "Region / State / Province",
    "woeid": 24554868,
    "latt_long": "52.883560,-1.974060"
  },
  "sources": [
    {
      "title": "BBC",
      "slug": "bbc",
      "url": "http://www.bbc.co.uk/weather/",
      "crawl_rate": 180
    },
    {
      "title": "Forecast.io",
      "slug": "forecast-io",
      "url": "http://forecast.io/",
      "crawl_rate": 480
    },
    {
      "title": "HAMweather",
      "slug": "hamweather",
      "url": "http://www.hamweather.com/",
      "crawl_rate": 360
    },
    {
      "title": "Met Office",
      "slug": "met-office",
      "url": "http://www.metoffice.gov.uk/",
      "crawl_rate": 180
    },
    {
      "title": "OpenWeatherMap",
      "slug": "openweathermap",
      "url": "http://openweathermap.org/",
      "crawl_rate": 360
    },
    {
      "title": "Weather Underground",
      "slug": "wunderground",
      "url": "https://www.wunderground.com/?apiref=fc30dc3cd224e19b",
      "crawl_rate": 720
    },
    {
      "title": "World Weather Online",
      "slug": "world-weather-online",
      "url": "http://www.worldweatheronline.com/",
      "crawl_rate": 360
    },
    {
      "title": "Yahoo",
      "slug": "yahoo",
      "url": "http://weather.yahoo.com/",
      "crawl_rate": 180
    }
  ],
  "title": "London",
  "location_type": "City",
  "woeid": 44418,
  "latt_long": "51.506321,-0.12714",
  "timezone": "Europe/London"
}

Great, now that we know what our data is going to look like, let’s create the necessary data models.

Data Model

Even though the weather API returns weather for multiple days, for simplicity, we’re only going to worry about today’s weather.

We are going to extract a subset of the data from the API and create a Weather model which will look something like:

import 'package:equatable/equatable.dart';

enum WeatherCondition {
  snow,
  sleet,
  hail,
  thunderstorm,
  heavyRain,
  lightRain,
  showers,
  heavyCloud,
  lightCloud,
  clear,
  unknown
}

class Weather extends Equatable {
  final WeatherCondition condition;
  final String formattedCondition;
  final double minTemp;
  final double temp;
  final double maxTemp;
  final int locationId;
  final String created;
  final DateTime lastUpdated;
  final String location;

  Weather({
    this.condition,
    this.formattedCondition,
    this.minTemp,
    this.temp,
    this.maxTemp,
    this.locationId,
    this.created,
    this.lastUpdated,
    this.location,
  }) : super([
          condition,
          formattedCondition,
          minTemp,
          temp,
          maxTemp,
          locationId,
          created,
          lastUpdated,
          location,
        ]);

  static Weather fromJson(dynamic json) {
    final consolidatedWeather = json['consolidated_weather'][0];
    return Weather(
      condition: _mapStringToWeatherCondition(
          consolidatedWeather['weather_state_abbr']),
      formattedCondition: consolidatedWeather['weather_state_name'],
      minTemp: consolidatedWeather['min_temp'] as double,
      temp: consolidatedWeather['the_temp'] as double,
      maxTemp: consolidatedWeather['max_temp'] as double,
      locationId: json['woeid'] as int,
      created: consolidatedWeather['created'],
      lastUpdated: DateTime.now(),
      location: json['title'],
    );
  }

  static WeatherCondition _mapStringToWeatherCondition(String input) {
    WeatherCondition state;
    switch (input) {
      case 'sn':
        state = WeatherCondition.snow;
        break;
      case 'sl':
        state = WeatherCondition.sleet;
        break;
      case 'h':
        state = WeatherCondition.hail;
        break;
      case 't':
        state = WeatherCondition.thunderstorm;
        break;
      case 'hr':
        state = WeatherCondition.heavyRain;
        break;
      case 'lr':
        state = WeatherCondition.lightRain;
        break;
      case 's':
        state = WeatherCondition.showers;
        break;
      case 'hc':
        state = WeatherCondition.heavyCloud;
        break;
      case 'lc':
        state = WeatherCondition.lightCloud;
        break;
      case 'c':
        state = WeatherCondition.clear;
        break;
      default:
        state = WeatherCondition.unknown;
    }
    return state;
  }
}

We extend [_Equatable_]([https://pub.dartlang.org/packages/equatable)](https://pub.dartlang.org/packages/equatable) "https://pub.dartlang.org/packages/equatable)") so that we can compare _Weather_ instances. By default, the equality operator returns true if and only if this and other are the same instance.

There’s not much happening here; we are just defining our Weather data model and implementing a fromJson method so that we can create a Weather instance from the API response body.

Next, we need to build our WeatherApiClient which will be responsible for making http requests to the weather API.

Data Provider

The WeatherApiClient is the lowest layer in our application architecture (the data provider). Its only responsibility is to fetch data directly from our API.

As we mentioned earlier, we are going to be hitting two endpoints so our WeatherApiClient needs to expose two public methods:

  • /api/location/search/?query=$city to get a locationId for a given city name
  • /api/location/$locationId to get the weather for a given locationId

It should look something like this:

import 'dart:convert';

import 'package:meta/meta.dart';
import 'package:http/http.dart' as http;

import 'package:flutter_weather/models/models.dart';

class WeatherApiClient {
  static const baseUrl = 'https://www.metaweather.com';
  final http.Client httpClient;  

  WeatherApiClient({@required this.httpClient}) : assert(httpClient != null);

  Future<int> getLocationId(String city) async {
    final locationUrl = '$baseUrl/api/location/search/?query=$city';
    final locationResponse = await this.httpClient.get(locationUrl);
    if (locationResponse.statusCode != 200) {
      throw Exception('error getting locationId for city');
    }

    final locationJson = jsonDecode(locationResponse.body) as List;
    return (locationJson.first)['woeid'];
  }

  Future<Weather> fetchWeather(int locationId) async {
    final weatherUrl = '$baseUrl/api/location/$locationId';
    final weatherResponse = await this.httpClient.get(weatherUrl);

    if (weatherResponse.statusCode != 200) {
      throw Exception('error getting weather for location');
    }

    final weatherJson = jsonDecode(weatherResponse.body);
    return Weather.fromJson(weatherJson);
  }
}

Note: Our _WeatherApiClient_ has an _httpClient_ injected via the constructor and it handles making the network request and serializing the response json into the respective data model.

We’ve got our DataProvider done so it’s time to move up to the next layer of our app’s architecture: the repository layer.

Repository

The WeatherRepository serves as an abstraction between the client code and the data provider so that as a developer working on features, you don’t have to know where the data is coming from.

Our WeatherRepository will have a dependency on our WeatherApiClient that we just created and it will expose a single public method called, you guessed it, getWeather(String city).

No one needs to know that under the hood we need to make two API calls (one for locationId and one for weather) because no one really cares. All we care about is getting the Weather for a given city.

Our WeatherRepository is quite simple and should look something like this:

import 'package:meta/meta.dart';

import 'package:flutter_weather/repositories/weather_api_client.dart';
import 'package:flutter_weather/models/models.dart';

class WeatherRepository {
  final WeatherApiClient weatherApiClient;

  WeatherRepository({@required this.weatherApiClient})
      : assert(weatherApiClient != null);

  Future<Weather> getWeather(String city) async {
    final int locationId = await weatherApiClient.getLocationId(city);
    return weatherApiClient.fetchWeather(locationId);
  }
}

Awesome! We are now ready to move up to the business logic layer and start building our WeatherBloc.

Business Logic (Bloc)

Our WeatherBloc is responsible for receiving WeatherEvents and converting them into WeatherStates. It will have a dependency on WeatherRepository so that it can retrieve the Weather when a user inputs a city of their choice.

Before jumping into the Bloc we need to define what events our WeatherBloc will be handling as well as how we are going to represent our WeatherState.

Weather Event

For simplicity, we’re going to start off by having a single event called FetchWeather.

We can define it like:

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

abstract class WeatherEvent extends Equatable {
  WeatherEvent([List props = const []]) : super(props);
}

class FetchWeather extends WeatherEvent {
  final String city;

  FetchWeather({@required this.city})
      : assert(city != null),
        super([city]);
}

Whenever a user inputs a city, we will dispatch a FetchWeather event with the given city and our bloc will responsible for figuring out what the weather is there and returning a new WeatherState.

Weather State

For the current application, we will have 4 possible states:

  • /api/location/search/?query=$city to get a locationId for a given city name
  • /api/location/$locationId to get the weather for a given locationId

We can represent these states like so:

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

import 'package:flutter_weather/models/models.dart';

abstract class WeatherState extends Equatable {
  WeatherState([List props = const []]) : super(props);
}

class WeatherEmpty extends WeatherState {}

class WeatherLoading extends WeatherState {}

class WeatherLoaded extends WeatherState {
  final Weather weather;

  WeatherLoaded({@required this.weather})
      : assert(weather != null),
        super([weather]);
}

class WeatherError extends WeatherState {}

Now that we have our Events and our States defined and implemented we are ready to make our WeatherBloc.

Weather Bloc

Our WeatherBloc is very straightforward. To recap, it converts WeatherEvents into WeatherStates and has a dependency on the WeatherRepository.

Tip: Check out the Bloc VSCode Extension in order to take advantage of the bloc snippets and even further improve your efficiency and development speed.

import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';

import 'package:flutter_weather/repositories/repositories.dart';
import 'package:flutter_weather/models/models.dart';

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository weatherRepository;

  WeatherBloc({@required this.weatherRepository})
      : assert(weatherRepository != null);

  @override
  WeatherState get initialState => WeatherEmpty();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherState currentState,
    WeatherEvent event,
  ) async* {
    if (event is FetchWeather) {
      yield WeatherLoading();
      try {
        final Weather weather = await weatherRepository.getWeather(event.city);
        yield WeatherLoaded(weather: weather);
      } catch (_) {
        yield WeatherError();
      }
    }
  }
}

We set our initialState to WeatherEmpty since initially, the user has not selected a city. Then, all that’s left is to implement mapEventToState.

Since we are only handling the FetchWeather event all we need to do is yield our WeatherLoading state when we get a FetchWeather event and then try to get the weather from the WeatherRepository.

If we are able to successfully retrieve the weather we then yield a WeatherLoaded state and if we are unable to retrieve the weather, we yield a WeatherError state.

That’s all there is to it! Now we’re ready to move on to the final layer: the presentation layer.

Presentation

Setup

As you’ve probably already seen in other tutorials, we’re going to create a SimpleBlocDelegate so that we can see all state transitions in our application.

import 'package:bloc/bloc.dart';

class SimpleBlocDelegate extends BlocDelegate {
  @override
  onTransition(Transition transition) {
    print(transition);
  }
}

Next, we’re going to set our delegate in our main function like so:

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();  
}

Lastly, we need to create our WeatherRepository and inject it into our App widget.

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();

  final WeatherRepository weatherRepository = WeatherRepository(
    weatherApiClient: WeatherApiClient(
      httpClient: http.Client(),
    ),
  );

  runApp(App(weatherRepository: weatherRepository));
}

App

Our App widget is going to start off as a StatelessWidget which has the WeatherRepository injected and builds the MaterialApp with our Weather widget.

class App extends StatelessWidget {
  final WeatherRepository weatherRepository;

  App({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Weather',
      home: Weather(
        weatherRepository: weatherRepository,
      ),
    );
  }
}

Weather

Our Weather Widget will be a StatefulWidget responsible for creating and disposing a WeatherBloc.

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/widgets/widgets.dart';
import 'package:flutter_weather/repositories/repositories.dart';
import 'package:flutter_weather/blocs/blocs.dart';

class Weather extends StatefulWidget {
  final WeatherRepository weatherRepository;

  Weather({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<Weather> createState() => _WeatherState();
}

class _WeatherState extends State<Weather> {
  WeatherBloc _weatherBloc;

  @override
  void initState() {
    super.initState();
    _weatherBloc = WeatherBloc(weatherRepository: widget.weatherRepository);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Weather'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () async {
              final city = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => CitySelection(),
                ),
              );
              if (city != null) {
                _weatherBloc.dispatch(FetchWeather(city: city));
              }
            },
          )
        ],
      ),
      body: Center(
        child: BlocBuilder(
          bloc: _weatherBloc,
          builder: (_, WeatherState state) {
            if (state is WeatherEmpty) {
              return Center(child: Text('Please Select a Location'));
            }
            if (state is WeatherLoading) {
              return Center(child: CircularProgressIndicator());
            }
            if (state is WeatherLoaded) {
              final weather = state.weather;

              return ListView(
                children: <Widget>[
                  Padding(
                    padding: EdgeInsets.only(top: 100.0),
                    child: Center(
                      child: Location(location: weather.location),
                    ),
                  ),
                  Center(
                    child: LastUpdated(dateTime: weather.lastUpdated),
                  ),
                  Padding(
                    padding: EdgeInsets.symmetric(vertical: 50.0),
                    child: Center(
                      child: CombinedWeatherTemperature(
                        weather: weather,
                      ),
                    ),
                  ),
                ],
              );
            }
            if (state is WeatherError) {
              return Text(
                'Something went wrong!',
                style: TextStyle(color: Colors.red),
              );
            }
          },
        ),
      ),
    );
  }

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

All that’s happening in this widget is we’re using BlocBuilder with our WeatherBloc in order to rebuild our UI based on state changes in our WeatherBloc.

You’ll notice that we are referencing a Location, LastUpdated, and CombinedWeatherTemperature widget which we will create in the following sections.

Location

Our Location widget is simple; it displays the current location.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';

class Location extends StatelessWidget {
  final String location;

  Location({Key key, @required this.location})
      : assert(location != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      location,
      style: TextStyle(
        fontSize: 30,
        fontWeight: FontWeight.bold,
        color: Colors.white,
      ),
    );
  }
}

Last Updated

Our LastUpdated widget is also super simple; it displays the last updated time so that users know how fresh the weather data is.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';

class LastUpdated extends StatelessWidget {
  final DateTime dateTime;

  LastUpdated({Key key, @required this.dateTime})
      : assert(dateTime != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      'Updated: ${TimeOfDay.fromDateTime(dateTime).format(context)}',
      style: TextStyle(
        fontSize: 18,
        fontWeight: FontWeight.w200,
        color: Colors.white,
      ),
    );
  }
}

Note: We are using [_TimeOfDay_]([https://docs.flutter.io/flutter/material/TimeOfDay-class.html)](https://docs.flutter.io/flutter/material/TimeOfDay-class.html) "https://docs.flutter.io/flutter/material/TimeOfDay-class.html)") to format the _DateTime_ into a more human-readable format.

Combined Weather Temperature

The CombinedWeatherTemperature widget is a compositional widget which displays the current weather along with the temperature. We are still going to modularize the Temperature and WeatherConditions widgets so that they can all be reused.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';

import 'package:flutter_weather/models/models.dart' as model;
import 'package:flutter_weather/widgets/widgets.dart';

class CombinedWeatherTemperature extends StatelessWidget {
  final model.Weather weather;

  CombinedWeatherTemperature({
    Key key,
    @required this.weather,
  })  : assert(weather != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(20.0),
              child: WeatherConditions(condition: weather.condition),
            ),
            Padding(
              padding: EdgeInsets.all(20.0),
              child: Temperature(
                temperature: weather.temp,
                high: weather.maxTemp,
                low: weather.minTemp,
              ),
            ),
          ],
        ),
        Center(
          child: Text(
            weather.formattedCondition,
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.w200,
              color: Colors.white,
            ),
          ),
        ),
      ],
    );
  }
}

Note: We are using two unimplemented widgets: _WeatherConditions_ and _Temperature_ which we will create next.

Weather Conditions

Our WeatherConditions widget will be responsible for displaying the current weather conditions (clear, showers, thunderstorms, etc…) with an icon.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';

import 'package:flutter_weather/models/models.dart';

class WeatherConditions extends StatelessWidget {
  final WeatherCondition condition;

  WeatherConditions({Key key, @required this.condition})
      : assert(condition != null),
        super(key: key);

  @override
  Widget build(BuildContext context) => _mapConditionToImage(condition);    

  Image _mapConditionToImage(WeatherCondition condition) {
    Image image;
    switch (condition) {
      case WeatherCondition.clear:
      case WeatherCondition.lightCloud:
        image = Image.asset('assets/clear.png');
        break;
      case WeatherCondition.hail:
      case WeatherCondition.snow:
      case WeatherCondition.sleet:
        image = Image.asset('assets/snow.png');
        break;
      case WeatherCondition.heavyCloud:
        image = Image.asset('assets/cloudy.png');
        break;
      case WeatherCondition.heavyRain:
      case WeatherCondition.lightRain:
      case WeatherCondition.showers:
        image = Image.asset('assets/rainy.png');
        break;
      case WeatherCondition.thunderstorm:
        image = Image.asset('assets/thunderstorm.png');
        break;
      case WeatherCondition.unknown:
        image = Image.asset('assets/clear.png');
        break;
    }
    return image;
  }
}

Tip: Check out icons8 for the assets used in this tutorial.

Temperature

Our Temperature widget will be responsible for displaying the average, min, and max temperatures.

import 'package:flutter/material.dart';

class Temperature extends StatelessWidget {
  final double temperature;
  final double low;
  final double high;

  Temperature({
    Key key,
    this.temperature,
    this.low,
    this.high,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Padding(
          padding: EdgeInsets.only(right: 20.0),
          child: Text(
            '${_formattedTemperature(temperature)}°',
            style: TextStyle(
              fontSize: 32,
              fontWeight: FontWeight.w600,
              color: Colors.white,
            ),
          ),
        ),
        Column(
          children: [
            Text(
              'max: ${_formattedTemperature(high)}°',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w100,
                color: Colors.white,
              ),
            ),
            Text(
              'min: ${_formattedTemperature(low)}°',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w100,
                color: Colors.white,
              ),
            )
          ],
        )
      ],
    );
  }

  int _formattedTemperature(double t) => t.round();
}

The last thing we need to implement to have a functional app is our CitySelection widget which allows users to type in the name of a city.

City Selection

The CitySelection widget will allow users to input a city name and pass the selected city back to the App widget.

import 'package:flutter/material.dart';

class CitySelection extends StatefulWidget {
  @override
  State<CitySelection> createState() => _CitySelectionState();
}

class _CitySelectionState extends State<CitySelection> {
  final TextEditingController _textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('City'),
      ),
      body: Form(
        child: Row(
          children: [
            Expanded(
              child: Padding(
                padding: EdgeInsets.only(left: 10.0),
                child: TextFormField(
                  controller: _textController,
                  decoration: InputDecoration(
                    labelText: 'City',
                    hintText: 'Chicago',
                  ),
                ),
              ),
            ),
            IconButton(
              icon: Icon(Icons.search),
              onPressed: () {
                Navigator.pop(context, _textController.text);
              },
            )
          ],
        ),
      ),
    );
  }
}

CitySelection needs to be a StatefulWidget because it has to maintain a TextController.

Note: When we press the search button we use _Navigator.pop_ and pass the current text from our _TextController_ back to the previous view.

At this point we have a fully functioning weather app but upon running it you’ll notice it has a few problems:

  • /api/location/search/?query=$city to get a locationId for a given city name
  • /api/location/$locationId to get the weather for a given locationId

Let’s address these problems and take our Weather App to the next level!

Pull-To-Refresh

In order to support pull-to-refresh we will need to update our WeatherBloc to handle a second event: RefreshWeather.

class RefreshWeather extends WeatherEvent {
  final String city;

  RefreshWeather({@required this.city})
      : assert(city != null),
        super([city]);
}

Next, we need to update our mapEventToState to handle a RefreshWeather event.

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository weatherRepository;

  WeatherBloc({@required this.weatherRepository})
      : assert(weatherRepository != null);

  @override
  WeatherState get initialState => WeatherEmpty();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherState currentState,
    WeatherEvent event,
  ) async* {
    if (event is FetchWeather) {
      yield WeatherLoading();
      try {
        final Weather weather = await weatherRepository.getWeather(event.city);
        yield WeatherLoaded(weather: weather);
      } catch (_) {
        yield WeatherError();
      }
    }

    if (event is RefreshWeather) {
      try {
        final Weather weather = await weatherRepository.getWeather(event.city);
        yield WeatherLoaded(weather: weather);
      } catch (_) {
        yield currentState;
      }
    }
  }
}

Lastly, we need to update our presentation layer to use a RefreshIndicator widget.

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/widgets/widgets.dart';
import 'package:flutter_weather/repositories/repositories.dart';
import 'package:flutter_weather/blocs/blocs.dart';

class Weather extends StatefulWidget {
  final WeatherRepository weatherRepository;

  Weather({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<Weather> createState() => _WeatherState();
}

class _WeatherState extends State<Weather> {
  WeatherBloc _weatherBloc;
  Completer<void> _refreshCompleter;

  @override
  void initState() {
    super.initState();
    _refreshCompleter = Completer<void>();
    _weatherBloc = WeatherBloc(weatherRepository: widget.weatherRepository);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Weather'),
        actions: <Widget>[          
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () async {
              final city = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => CitySelection(),
                ),
              );
              if (city != null) {
                _weatherBloc.dispatch(FetchWeather(city: city));
              }
            },
          )
        ],
      ),
      body: Center(
        child: BlocBuilder(
          bloc: _weatherBloc,
          builder: (_, WeatherState state) {
            if (state is WeatherEmpty) {
              return Center(child: Text('Please Select a Location'));
            }
            if (state is WeatherLoading) {
              return Center(child: CircularProgressIndicator());
            }
            if (state is WeatherLoaded) {
              final weather = state.weather;              

              _refreshCompleter?.complete();
              _refreshCompleter = Completer();

              return RefreshIndicator(
                onRefresh: () {
                  _weatherBloc.dispatch(
                    RefreshWeather(city: state.weather.location),
                  );
                  return _refreshCompleter.future;
                },
                child: ListView(
                  children: <Widget>[
                    Padding(
                      padding: EdgeInsets.only(top: 100.0),
                      child: Center(
                        child: Location(location: weather.location),
                      ),
                    ),
                    Center(
                      child: LastUpdated(dateTime: weather.lastUpdated),
                    ),
                    Padding(
                      padding: EdgeInsets.symmetric(vertical: 50.0),
                      child: Center(
                        child: CombinedWeatherTemperature(
                          weather: weather,
                        ),
                      ),
                    ),
                  ],
                ),
              );
            }
            if (state is WeatherError) {
              return Text(
                'Something went wrong!',
                style: TextStyle(color: Colors.red),
              );
            }
          },
        ),
      ),
    );
  }

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

In order to use the RefreshIndicator we had to create a [Completer]([https://api.dartlang.org/stable/2.1.0/dart-async/Completer-class.html)](https://api.dartlang.org/stable/2.1.0/dart-async/Completer-class.html) "https://api.dartlang.org/stable/2.1.0/dart-async/Completer-class.html)") which allows us to produce a Future which we can complete at a later time.

That’s it! We now have solved problem #1 and users can refresh the weather by pulling down.

Next, let’s tackle the plain looking UI by creating a ThemeBloc.

Dynamic Theme

Our ThemeBloc is going to be responsible for converting ThemeEvents into ThemeStates.

Our ThemeEvents are going to consist of a single event called WeatherChanged which will be dispatched whenever the weather conditions we are displaying have changed.

abstract class ThemeEvent extends Equatable {
  ThemeEvent([List props = const []]) : super(props);
}

class WeatherChanged extends ThemeEvent {
  final WeatherCondition condition;

  WeatherChanged({@required this.condition})
      : assert(condition != null),
        super([condition]);
}

Our ThemeState will consist of a ThemeData and a MaterialColor which we will use to enhance our UI.

class ThemeState extends Equatable {
  final ThemeData theme;
  final MaterialColor color;

  ThemeState({@required this.theme, @required this.color})
      : assert(theme != null),
        assert(color != null),
        super([theme, color]);
}

Now, we can implement our ThemeBloc which should look like:

class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
  @override
  ThemeState get initialState => ThemeState(
        theme: ThemeData.light(),
        color: Colors.lightBlue,
      );

  @override
  Stream<ThemeState> mapEventToState(
    ThemeState currentState,
    ThemeEvent event,
  ) async* {
    if (event is WeatherChanged) {
      yield _mapWeatherConditionToThemeData(event.condition);
    }
  }

  ThemeState _mapWeatherConditionToThemeData(WeatherCondition condition) {
    ThemeState theme;
    switch (condition) {
      case WeatherCondition.clear:
      case WeatherCondition.lightCloud:
        theme = ThemeState(
          theme: ThemeData(
            primaryColor: Colors.orangeAccent,
          ),
          color: Colors.yellow,
        );
        break;
      case WeatherCondition.hail:
      case WeatherCondition.snow:
      case WeatherCondition.sleet:
        theme = ThemeState(
          theme: ThemeData(
            primaryColor: Colors.lightBlueAccent,
          ),
          color: Colors.lightBlue,
        );
        break;
      case WeatherCondition.heavyCloud:
        theme = ThemeState(
          theme: ThemeData(
            primaryColor: Colors.blueGrey,
          ),
          color: Colors.grey,
        );
        break;
      case WeatherCondition.heavyRain:
      case WeatherCondition.lightRain:
      case WeatherCondition.showers:
        theme = ThemeState(
          theme: ThemeData(
            primaryColor: Colors.indigoAccent,
          ),
          color: Colors.indigo,
        );
        break;
      case WeatherCondition.thunderstorm:
        theme = ThemeState(
          theme: ThemeData(
            primaryColor: Colors.deepPurpleAccent,
          ),
          color: Colors.deepPurple,
        );
        break;
      case WeatherCondition.unknown:
        theme = ThemeState(
          theme: ThemeData.light(),
          color: Colors.lightBlue,
        );
        break;
    }
    return theme;
  }
}

Even though it’s a lot of code, the only thing in here is logic to convert a WeatherCondition to a new ThemeState.

We can now update our App widget to create a ThemeBloc and use BlocBuilder to react to changes in ThemeState.

Since our **App** widget will now be responsible for creating and disposing of a **ThemeBloc** we need to refactor it into a **StatefulWidget**.

class App extends StatefulWidget {
  final WeatherRepository weatherRepository;

  App({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  ThemeBloc _themeBloc = ThemeBloc();  

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: _themeBloc,
      child: BlocBuilder(
        bloc: _themeBloc,
        builder: (_, ThemeState themeState) {
          return MaterialApp(
            title: 'Flutter Weather',
            theme: themeState.theme,
            home: Weather(
              weatherRepository: widget.weatherRepository,
            ),
          );
        },
      ),
    );
  }

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

Note: We are using _BlocProvider_ to make our _ThemeBloc_ globally available using _BlocProvider.of<ThemeBloc>(context)_.

The last thing we need to do is create a cool GradientContainer widget which will color our background with respect to the current weather conditions.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';

class GradientContainer extends StatelessWidget {
  final Widget child;
  final MaterialColor color;

  const GradientContainer({
    Key key,
    @required this.color,
    @required this.child,
  })  : assert(color != null, child != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          stops: [0.6, 0.8, 1.0],
          colors: [
            color[700],
            color[500],
            color[300],
          ],
        ),
      ),
      child: child,
    );
  }
}

Now we can use our GradientContainer in our Weather widget like so:

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/widgets/widgets.dart';
import 'package:flutter_weather/repositories/repositories.dart';
import 'package:flutter_weather/blocs/blocs.dart';

class Weather extends StatefulWidget {
  final WeatherRepository weatherRepository;

  Weather({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<Weather> createState() => _WeatherState();
}

class _WeatherState extends State<Weather> {
  WeatherBloc _weatherBloc;
  Completer<void> _refreshCompleter;

  @override
  void initState() {
    super.initState();
    _refreshCompleter = Completer<void>();
    _weatherBloc = WeatherBloc(weatherRepository: widget.weatherRepository);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Weather'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            onPressed: () async {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => Settings(),
                ),
              );
            },
          ),
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () async {
              final city = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => CitySelection(),
                ),
              );
              if (city != null) {
                _weatherBloc.dispatch(FetchWeather(city: city));
              }
            },
          )
        ],
      ),
      body: Center(
        child: BlocBuilder(
          bloc: _weatherBloc,
          builder: (_, WeatherState state) {
            if (state is WeatherEmpty) {
              return Center(child: Text('Please Select a Location'));
            }
            if (state is WeatherLoading) {
              return Center(child: CircularProgressIndicator());
            }
            if (state is WeatherLoaded) {
              final weather = state.weather;
              final themeBloc = BlocProvider.of<ThemeBloc>(context);
              themeBloc.dispatch(WeatherChanged(condition: weather.condition));

              _refreshCompleter?.complete();
              _refreshCompleter = Completer();

              return BlocBuilder(
                bloc: themeBloc,
                builder: (_, ThemeState themeState) {
                  return GradientContainer(
                    color: themeState.color,
                    child: RefreshIndicator(
                      onRefresh: () {
                        _weatherBloc.dispatch(
                          RefreshWeather(city: state.weather.location),
                        );
                        return _refreshCompleter.future;
                      },
                      child: ListView(
                        children: <Widget>[
                          Padding(
                            padding: EdgeInsets.only(top: 100.0),
                            child: Center(
                              child: Location(location: weather.location),
                            ),
                          ),
                          Center(
                            child: LastUpdated(dateTime: weather.lastUpdated),
                          ),
                          Padding(
                            padding: EdgeInsets.symmetric(vertical: 50.0),
                            child: Center(
                              child: CombinedWeatherTemperature(
                                weather: weather,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            }
            if (state is WeatherError) {
              return Text(
                'Something went wrong!',
                style: TextStyle(color: Colors.red),
              );
            }
          },
        ),
      ),
    );
  }

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

We are accessing our ThemeBloc via BlocProvider.of<ThemeBloc>(context) and are then dispatching a WeatherChanged event on each WeatherLoad.

We also wrapped our GradientContainer widget with a BlocBuilder of ThemeBloc so that we can rebuild the GradientContainer and it’s children in response to ThemeState changes.

Awesome! We now have an app that looks way nicer (in my opinion 😛) and have tackled problem #2.

All that’s left is to handle unit conversion between Celsius and Fahrenheit. To do that we’ll create a Settings widget and a SettingsBloc.

Unit Conversion

We’ll start off by creating our SettingsBloc which will convert SettingsEvents into SettingsStates.

Our SettingsEvents will consist of a single event: TemperatureUnitsToggled.

abstract class SettingsEvent extends Equatable {}

class TemperatureUnitsToggled extends SettingsEvent {}

Our SettingsState will simply consist of the current TemperatureUnits.

enum TemperatureUnits { fahrenheit, celsius }

class SettingsState extends Equatable {
  final TemperatureUnits temperatureUnits;

  SettingsState({@required this.temperatureUnits})
      : assert(temperatureUnits != null),
        super([temperatureUnits]);
}

Lastly, we need to create our SettingsBloc:

class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
  @override
  SettingsState get initialState =>
      SettingsState(temperatureUnits: TemperatureUnits.celsius);

  @override
  Stream<SettingsState> mapEventToState(
    SettingsState currentState,
    SettingsEvent event,
  ) async* {
    if (event is TemperatureUnitsToggled) {
      yield SettingsState(
        temperatureUnits:
            currentState.temperatureUnits == TemperatureUnits.celsius
                ? TemperatureUnits.fahrenheit
                : TemperatureUnits.celsius,
      );
    }
  }
}

All we’re doing is using Fahrenheit if TemperatureUnitsToggled is dispatched and the current units are Celsius and vice versa.

Now we need to add our SettingsBloc to our App widget.

class App extends StatefulWidget {
  final WeatherRepository weatherRepository;

  App({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  ThemeBloc _themeBloc = ThemeBloc();
  SettingsBloc _settingsBloc = SettingsBloc();

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: _themeBloc,
      child: BlocProvider(
        bloc: _settingsBloc,
        child: BlocBuilder(
          bloc: _themeBloc,
          builder: (_, ThemeState themeState) {
            return MaterialApp(
              title: 'Flutter Demo',
              theme: themeState.theme,
              home: Weather(
                weatherRepository: widget.weatherRepository,
              ),
            );
          },
        ),
      ),
    );
  }

  @override
  void dispose() {
    _themeBloc.dispose();
    _settingsBloc.dispose();
    super.dispose();
  }
}

Again, we’re making SettingsBloc globally accessible using BlocProvider and we are also disposing it in the dispose override.

Now we need to create our Settings widget from which users can toggle the units.

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/blocs/blocs.dart';

class Settings extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final settingsBloc = BlocProvider.of<SettingsBloc>(context);
    return Scaffold(
      appBar: AppBar(title: Text('Settings')),
      body: ListView(
        children: <Widget>[
          BlocBuilder(
              bloc: settingsBloc,
              builder: (_, SettingsState state) {
                return ListTile(
                  title: Text(
                    'Temperature Units',
                  ),
                  isThreeLine: true,
                  subtitle:
                      Text('Use metric measurements for temperature units.'),
                  trailing: Switch(
                    value: state.temperatureUnits == TemperatureUnits.celsius,
                    onChanged: (_) =>
                        settingsBloc.dispatch(TemperatureUnitsToggled()),
                  ),
                );
              }),
        ],
      ),
    );
  }
}

We’re using BlocProvider to access the SettingsBloc via the BuildContext and then using BlocBuilder to rebuild our UI based on SettingsState changed.

Our UI consists of a ListView with a single ListTile which contains a Switch that users can toggle to select Celsius vs. Fahrenheit.

Note: In the switch’s _onChanged_ method we dispatch a _TemperatureUnitsToggled_ event to notify the _SettingsBloc_ that the temperature units have changed.

Next, we need to allow users to get to the Settings widget from our Weather widget.

We can do that by adding a new IconButton in our AppBar.

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/widgets/widgets.dart';
import 'package:flutter_weather/repositories/repositories.dart';
import 'package:flutter_weather/blocs/blocs.dart';

class Weather extends StatefulWidget {
  final WeatherRepository weatherRepository;

  Weather({Key key, @required this.weatherRepository})
      : assert(weatherRepository != null),
        super(key: key);

  @override
  State<Weather> createState() => _WeatherState();
}

class _WeatherState extends State<Weather> {
  WeatherBloc _weatherBloc;
  Completer<void> _refreshCompleter;

  @override
  void initState() {
    super.initState();
    _refreshCompleter = Completer<void>();
    _weatherBloc = WeatherBloc(weatherRepository: widget.weatherRepository);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Weather'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => Settings(),
                ),
              );
            },
          ),
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () async {
              final city = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => CitySelection(),
                ),
              );
              if (city != null) {
                _weatherBloc.dispatch(FetchWeather(city: city));
              }
            },
          )
        ],
      ),
      body: Center(
        child: BlocBuilder(
          bloc: _weatherBloc,
          builder: (_, WeatherState state) {
            if (state is WeatherEmpty) {
              return Center(child: Text('Please Select a Location'));
            }
            if (state is WeatherLoading) {
              return Center(child: CircularProgressIndicator());
            }
            if (state is WeatherLoaded) {
              final weather = state.weather;
              final themeBloc = BlocProvider.of<ThemeBloc>(context);
              themeBloc.dispatch(WeatherChanged(condition: weather.condition));

              _refreshCompleter?.complete();
              _refreshCompleter = Completer();

              return BlocBuilder(
                bloc: themeBloc,
                builder: (_, ThemeState themeState) {
                  return GradientContainer(
                    color: themeState.color,
                    child: RefreshIndicator(
                      onRefresh: () {
                        _weatherBloc.dispatch(
                          RefreshWeather(city: state.weather.location),
                        );
                        return _refreshCompleter.future;
                      },
                      child: ListView(
                        children: <Widget>[
                          Padding(
                            padding: EdgeInsets.only(top: 100.0),
                            child: Center(
                              child: Location(location: weather.location),
                            ),
                          ),
                          Center(
                            child: LastUpdated(dateTime: weather.lastUpdated),
                          ),
                          Padding(
                            padding: EdgeInsets.symmetric(vertical: 50.0),
                            child: Center(
                              child: CombinedWeatherTemperature(
                                weather: weather,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            }
            if (state is WeatherError) {
              return Text(
                'Something went wrong!',
                style: TextStyle(color: Colors.red),
              );
            }
          },
        ),
      ),
    );
  }

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

We’re almost done! We just need to update our Temperature widget to respond to the current units.

import 'package:flutter/material.dart';

import 'package:flutter_weather/blocs/blocs.dart';

class Temperature extends StatelessWidget {
  final double temperature;
  final double low;
  final double high;
  final TemperatureUnits units;

  Temperature({
    Key key,
    this.temperature,
    this.low,
    this.high,
    this.units,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Padding(
          padding: EdgeInsets.only(right: 20.0),
          child: Text(
            '${_formattedTemperature(temperature)}°',
            style: TextStyle(
              fontSize: 32,
              fontWeight: FontWeight.w600,
              color: Colors.white,
            ),
          ),
        ),
        Column(
          children: [
            Text(
              'max: ${_formattedTemperature(high)}°',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w100,
                color: Colors.white,
              ),
            ),
            Text(
              'min: ${_formattedTemperature(low)}°',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w100,
                color: Colors.white,
              ),
            )
          ],
        )
      ],
    );
  }

  int _toFahrenheit(double celsius) => ((celsius * 9 / 5) + 32).round();

  int _formattedTemperature(double t) =>
      units == TemperatureUnits.fahrenheit ? _toFahrenheit(t) : t.round();
}

And lastly, we need to inject the TemperatureUnits into the Temperature widget.

import 'package:flutter/material.dart';

import 'package:meta/meta.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:flutter_weather/blocs/blocs.dart';
import 'package:flutter_weather/models/models.dart' as model;
import 'package:flutter_weather/widgets/widgets.dart';

class CombinedWeatherTemperature extends StatelessWidget {
  final model.Weather weather;

  CombinedWeatherTemperature({
    Key key,
    @required this.weather,
  })  : assert(weather != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(20.0),
              child: WeatherConditions(condition: weather.condition),
            ),
            Padding(
              padding: EdgeInsets.all(20.0),
              child: BlocBuilder(
                bloc: BlocProvider.of<SettingsBloc>(context),
                builder: (_, SettingsState state) {
                  return Temperature(
                    temperature: weather.temp,
                    high: weather.maxTemp,
                    low: weather.minTemp,
                    units: state.temperatureUnits,
                  );
                },
              ),
            ),
          ],
        ),
        Center(
          child: Text(
            weather.formattedCondition,
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.w200,
              color: Colors.white,
            ),
          ),
        ),
      ],
    );
  }
}

🎉 The full source for this example can be found here. 🎉

If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.

Flutter Tutorial for Beginners - Build Android and iOS Apps with a Flutter Framework

Build Android and iOS apps with a flutter framework


You’ll learn

  • Better understanding of flutter and it’s basic widgets
  • Develop basic flutter application for android and iOS


Thanks for watching

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Flutter Tutorial - Flight List UI Example In Flutter

Flutter Tutorial for Beginners - Full Tutorial

Using Go Library in Flutter

Parsing JSON in Flutter

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

In this Flutter Tutorial for Beginners, you will learn how to build iOS & Android apps with Google Flutter and Dart

In this post, Flutter Tutorial for Beginners; you will learn how to build iOS & Android apps with Googles Flutter and Dart.

You don't need to learn Android/ Java and iOS/ Swift development to build real native mobile apps!

Flutter - a framework developed by Google - allows you to learn one language (Dart) and build beautiful native mobile apps in no time. Flutter is a SDK providing the tooling to compile that Dart code into native code and it also gives you a rich set of pre-built and pre-styled UI elements (so called widgets) which you can use to compose your user interfaces!

Let's get started in this video!

Take the full course on Udemy at a discount with the following link: http://bit.ly/2O5EaKu

Introduction to Flutter: Building iOS and Android Apps from a Single Codebase

Introduction to Flutter: Building iOS and Android Apps from a Single Codebase

Flutter allows developers to develop both Android and iOS apps using a single codebase. In this tutorial, we will introduce Flutter by building iOS and Android Apps from a Single Codebase.

Flutter allows developers to develop both Android and iOS apps using a single codebase. In this tutorial, we will introduce Flutter by building iOS and Android Apps from a Single Codebase.

Welcome to my first tutorial on Flutter. I have never written any post on cross-platform or hybrid app framework but Flutter has changed this mindset of mine.

Previously, I have developed on React Native, Cordova, Phone Gap, Ionic and now of these really work out for me until I found Flutter along with its huge community of developers and its showcase apps.

What is Flutter?

In a nutshell, it is a multi-layered system, such that higher layers are easier to use and allow you to express a lot with little code and lower layers give you more control at the expense of having to deal with some complexity.

Flutter Framework is written entirely in Dart. Most of the engine is written in C++, with Android-specific parts written in Java, and iOS-specific parts written in Objective-C. Like React Native, Flutter also provides reactive-style views, but Flutter takes a different approach to avoid performance problems caused by the need for a JavaScript bridge by using a compiled programming language, namely Dart.

Dart is compiled “ahead of time” (AOT) into native code for multiple platforms. This allows Flutter to communicate with the platform without going through a JavaScript bridge that does a context switch. It also compiles to native code which in turn improves app startup times.

In Flutter, it is all about Widgets. Widgets are the elements that affect and control the view and interface to an app.

Flutter renders the widget tree and paints it to a platform canvas. This is nice and simple (and fast). It’s Hot-Reload capability allows real-time development experience.

You can read more about Flutter and learn about its goodness here.

Getting Started

Today, we will be building a very simple Flutter app that can be deployed on both iOS & Android called Contactly as we go through this tutorial. This is a very simple Contacts List app which will demonstrate the capabilities of Flutter. Capabilities include:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

The final product of this app should look something like this:

It includes these features:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
The Flutter’s Project Structure

While you haven’t built any apps using Flutter, let me give you a quick overview of its project structure. Later when you create a Flutter project, you should see a project structure as such:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

I know you can’t wait to try out Flutter. Let’s dive in and set up all the required tools on your machine.

Installing Flutter

At the time of this writing, I’m using the following machine configuration and software version:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

I cannot guarantee that my tutorial will work for every configuration and platform, hence, I will not include configuration troubleshooting here to keep this tutorial short and objective-oriented.

First up, head over to Flutter Installation page to install Flutter. I will skip the steps here as the steps in the document is detailed enough.

Once you run flutter doctor and you got (1~4 checked), you are good to go! It’s not necessary to have Connected Devices checked.

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.0.0, on Mac OS X 10.13.6 17G4015, locale en-SG)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
[✓] Android Studio (version 3.2)
[✓] Connected device (2 available)

If you have encountered any errors like below, follow the suggested solutions to fix it. For example, if your Mac has not installed with Android Studio, head over to this website to download the software. Just make sure you have the first 4 items checked before moving on.

Creating a new Flutter Project

With Flutter installed, now let’s start to build your first Flutter project.

First, fire up Android Studio and click Start a new Flutter Project.

Next, select Flutter Application and click Next.

Then fill in Project name as contactly, or anything you like. By default, it should show your default path of the Flutter path. In case it doesn’t work for you, navigate and specify your own Flutter SDK path. Optionally, you can change your project location and give a simple description. Then, click Next.

Finally, fill in a Company domain. This will be replicated in your Bundle Identifier (iOS) & Package Name (Android). For my case, I checked both Kotlin & Swift support. Then, click Finish.

Trying out an App on iOS Simulator

Once you started your Flutter Application, some boilerplate code is automatically generated with a sample app that allows you to hit a button and perform some text updates. Before we make any code changes, it is a good checkpoint to try running it on your iOS simulator.

To run the app, find the dropdown list somewhere at the top right that says , click on it and select Open iOS Simulator.

Your last selected simulator hardware will be chosen, which is iPhone XR for my case.

Click Run, which is the green triangle, and the app should open in your simulator. You should be able to interact with the Demo app and push a few buttons!

Building the Main Page

With the demo app running successfully, we are now ready to start building our first Flutter App!

Let’s start by deleting all the code in main.dart. Yes! Press command-a to select the whole code snippet and hit Delete.

Now we will begin to write the code from scratch. First, insert the following line of code to import the material package:

import 'package:flutter/material.dart';

This package is essential for building the UI of the app. To ensure that the app knows what to run after it finishes launching, add the main() method like this:

void main() => runApp(ContactlyApp());

It’s always a good practice to organize files into separate packages and put the constants in a separate. So, let’s create the helper package and the Constants.dart file to place some of our constant values we will be using in this app.

Right-click on the lib folder and then select New > Package. Name the package helpers.

Now we have a separate folder to store our helper classes. To create a new dart file, right-click on helpers and then select New > File. Name it Constants.dart.

In Constants.dart, insert the following code:

import 'package:flutter/material.dart';
 
// Colors
Color appDarkGreyColor = Color.fromRGBO(58, 66, 86, 1.0);
 
// Strings
const appTitle = "Contactly";

Here we import the same material package, so we can use the Color declaration and declare an appTitle to be used app-wide.

Now head back to main.dart and add this import statement after the first import line.

import 'helpers/Constants.dart';

Let’s start building our Main Page by adding these lines of codes:

class ContactlyApp extends StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: appTitle,
      theme: new ThemeData(
        primaryColor: appDarkGreyColor,
      ),
    );
  }
 
}

MaterialApp is one of the convenience widgets which allows customisations like adding navigation routes, appBar etc. Setting debugShowCheckedModeBanner to false will get rid of the Red Debug label at the top right. We use our declared appTitle in our constant file here to give it a title. Then, we set the primaryColor.

All the code looks good here and you might be eager to try running it. If you really did, you will get a huge red-colored error screen!

This is because we are not yet ready to paint the canvas. Be Patient!

In most tutorials, they will guide you on building everything into main.dart. But I find that we could make it cleaner by separating each page into separate files, which you will be eventually doing so when building production-ready apps.

Meanwhile, Android Studio should indicate an error in the widget_test.dart file. Since we change the class name from MyApp to ContactlyApp, you should change the following line of code from:

await tester.pumpWidget(MyApp());

to:

await tester.pumpWidget(ContactlyApp());

Building the Login Page

Now let’s go ahead to create a new page called LoginPage.dart and place it under lib. Perform the same ritual of importing material package.

Here we will be creating a Stateless Widget since we don’t need to store any form of data. You can find more details about Stateless VS Stateful here.

Before we go into the code, let’s look at how the login screen should look like:

As you can see, the screen has the following components:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

To implement the screen component, insert the following code. Just copy & paste it first, we will go through them in awhile!

import 'package:flutter/material.dart';
import 'helpers/Constants.dart';
 
// 1
class LoginPage extends StatelessWidget {
 
  // 2
  final _pinCodeController = TextEditingController();
 
  // 3
  @override
  Widget build(BuildContext context) {
     // 3a
    final logo = CircleAvatar(
        backgroundColor: Colors.transparent,
        radius: bigRadius,
        child: appLogo,
    );
 
     // 3b
    final pinCode = TextFormField(
      controller: _pinCodeController,
      keyboardType: TextInputType.phone,
      maxLength: 4,
      maxLines: 1,
      autofocus: true,
      decoration: InputDecoration(
          hintText: pinCodeHintText,
          contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(32.0),
          ),
          hintStyle: TextStyle(
              color: Colors.white
          )
      ),
      style: TextStyle(
        color: Colors.white,
      ),
    );
 
     // 3c
    final loginButton = Padding(
      padding: EdgeInsets.symmetric(vertical: 16.0),
      child: RaisedButton(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(24),
        ),
        onPressed: () {},
        padding: EdgeInsets.all(12),
        color: appGreyColor,
        child: Text(loginButtonText, style: TextStyle(color: Colors.white)),
      ),
    );
 
     // 3d
    return Scaffold(
      backgroundColor: appDarkGreyColor,
      body: Center(
        child: ListView(
          shrinkWrap: true,
          padding: EdgeInsets.only(left: 24.0, right: 24.0),
          children: [
            logo,
            SizedBox(height: bigRadius),
            pinCode,
            SizedBox(height: buttonHeight),
            loginButton
          ],
        ),
      ),
    );
  }
}

And, for the Constants.dart file, please update it like this to add a number of constants that we use in the build method:

import 'package:flutter/material.dart';
 
// Colors
Color appDarkGreyColor = Color.fromRGBO(58, 66, 86, 1.0);
Color appGreyColor = Color.fromRGBO(64, 75, 96, .9);
 
// Strings
const appTitle = "Contactly";
const pinCodeHintText = "Pin Code";
const loginButtonText = "Login";
 
// Images
Image appLogo = Image.asset('assets/images/flutter-logo-round.png');
 
// Sizes
const bigRadius = 66.0;
const buttonHeight = 24.0;

OMG! That’s a huge chunk of code! Yes, but no worries. This is the first time we are really going deep into huge piles of the Dart code. Trust me, after going through these, you will get more familiar with how Flutter works 🙂

I have broken down this large piece of code into 3 major parts so that we can digest them easier:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

If you run the app now, you will probably end up with an error saying that the image asset cannot be loaded. We know the path is given to load the Image but there are 2 missing pieces: the image itself and the path that we need to include in pubspec.yaml.

First, you can get the logo image I use from here. Then, create a new directory called assets in the root directory, and create a sub-directory called images.

Your image should be placed in root/assets/images.

Then, go to pubspec.yaml and add the following code to inform the app what assets to bundle together during runtime so it can be loaded.

assets:
    - assets/images/flutter-logo-round.png

Please note that you must add the configuration above to the flutter: section like this:

flutter:
  assets:
    - assets/images/flutter-logo-round.png

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

That was like an Effiel Tower of Codes! UI codes are tough 😭

Before we run the app, we also need to tell our main() to run LoginPage as the home page. So, head back to main.dart and add home: LoginPage() after theme. Your build code should look like this:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: appTitle,
      theme: new ThemeData(
        primaryColor: appDarkGreyColor,
      ),
      home: LoginPage() // just added
    );
  }
 

Also, you will need to import LoginPage.dart at the very beginning of the file:

import 'LoginPage.dart';

Now run the app! You should see the Login Screen like this:

Cool, right? Let’s continue to build the rest of the screens.

Building Contacts List Page

Now we are warmed up a little, we can go a bit faster. We will now build the main feature of this app, the Contact List page. We will create a new file called HomePage.dart. Once you created the file, make sure you import material package:

import 'package:flutter/material.dart';

Contacts List Page will be a Stateful widget since we need to maintain the state of our contacts data. So add these first few lines of boilerplate codes:

class HomePage extends StatefulWidget {
 
  @override
  _HomePageState createState() {
    return _HomePageState();
  }
 
}
 
class _HomePageState extends State {
 
}

The first class HomePage will be called and used when navigating/presenting the page, while the private class _HomePageState will be called everytime the HomePage is called. This is also the mutable state object which we will maintain as the page get called.

Before we dive into coding again, let’s look at how our contact list screen looks like:

There are many things that we will need to do here:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

Setting up the Routing

Let’s hook up our navigation route between LoginPage & HomePage. Head over to Constants.dart and add these tags:

// Pages
const loginPageTag = 'Login Page';
const homePageTag = 'Home Page';

Then, go to main.dart and add these just before our build function:

  final routes = {
    loginPageTag: (context) => LoginPage(),
    homePageTag: (context) => HomePage(),
  };
 

You will also need to import the HomePage.dart file:

 	
import 'HomePage.dart';

The code above allows us to use tags to associate each individual page. 🙂 Finally, let’s add the routes to our build function just after home.

  Widget build(BuildContext context) {
     ...
     home: LoginPage(),
     routes: routes
    );
  }

We can’t really test this out yet as we have not implemented the UI for our ListView. So, let’s do that first.

Populate JSON data and map to ListView

For this demo, I store all the contact data in a JSON file. You can download the sample JSON file here and create a data folder under assets. Put the records.json file into the folder. Then, update pubspec.yaml with the below asset configuration:

  assets:
    - assets/images/flutter-logo-round.png
    - assets/data/records.json

Now that we have prepared the JSON data, we will need to create:

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

Record Class to hold a Contact

First, let’s create a new models package under lib and create a new file named Record.dart. You can insert these lines of code into the file:

class Record {
  String name;
  String address;
  String contact;
  String photo;
  String url;
 
  Record({
    this.name,
    this.address,
    this.contact,
    this.photo,
    this.url
  });
 
  factory Record.fromJson(Map json){
    return new Record(
        name: json['name'],
        address: json['address'],
        contact: json ['contact'],
        photo: json['photo'],
        url: json['url']
    );
  }
}

Dart provides factory constructors to support the factory pattern. The factory constructor is able to return values (objects). Here it parses the given JSON string and returns a Record instance, which represents a contact.

RecordList Class to hold the list of Contacts

In the same models package, create another file called RecordList.dart. Then, put in these lines of code:

import 'Record.dart';
 
class RecordList {
  List records = new List();
 
  RecordList({
    this.records
  });
 
  factory RecordList.fromJson(List parsedJson) {
 
    List records = new List();
 
    records = parsedJson.map((i) => Record.fromJson(i)).toList();
 
    return new RecordList(
      records: records,
    );
  }
}

RecordService Class to perform the loading task

Lastly, create another file named RecordService.dart in the same package and insert the following code:

import 'RecordList.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
 
class RecordService {
 
  Future _loadRecordsAsset() async {
    return await rootBundle.loadString('assets/data/records.json');
  }
 
  Future loadRecords() async {
    String jsonString = await _loadRecordsAsset();
    final jsonResponse = json.decode(jsonString);
    RecordList records = new RecordList.fromJson(jsonResponse);
    return records;
  }
 
}

Here, the loadRecords() function parses the records.json file and map it into a RecordList object, holding a list of Record objects. The keyword Future should be new to you if you are unfamiliar with Dart. To perform asynchronous operation in Dart, we use futures. Future objects (futures) represent the results of asynchronous operations.

Implementing the Home Page to list the Contacts

Now let’s use what we have implemented in our HomePage. Open the HomePage.dart and add these import statements at the very beginning:

import 'models/Record.dart';
import 'models/RecordList.dart';
import 'models/RecordService.dart';

Other than listing the contact records, the home page has a search feature that lets users search the contacts. So, first, declare the following variables in the _HomePageState class of the HomePage.dart file:

final TextEditingController _filter = new TextEditingController();
 
RecordList _records = new RecordList();
RecordList _filteredRecords = new RecordList();
 
String _searchText = "";
 
Icon _searchIcon = new Icon(Icons.search);
 
Widget _appBarTitle = new Text(appTitle);

Here is the purpose of each variable:

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

Since it’s a Stateful widget, we can add some small settings when the state is initialized:

@override
  void initState() {
    super.initState();
 
    _records.records = new List();
    _filteredRecords.records = new List();
 
    _getRecords();
  }
 
  void _getRecords() async {
    RecordList records = await RecordService().loadRecords();
    setState(() {
      for (Record record in records.records) {
        this._records.records.add(record);
        this._filteredRecords.records.add(record);
      }
    });
  } 

In the init state of the home page, we empty our records data and get fresh data from the JSON file. Here we don’t need to really use an Async Call, but it is to introduce its concept and how you could call it if you were to perform a data fetch from a server.

Remember that in our previous section, we return a Scaffold in the build function as the main UI structure. So, continue to insert the following code to create the UI structure:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildBar(context),
      backgroundColor: appDarkGreyColor,
      body: _buildList(context),
      resizeToAvoidBottomPadding: false,
    );
  }

Like most ListView pages we have seen in mobile apps, there is a navigation bar at the top. In the code above, the appBar is the navigation bar. We specify to call _buildBar(context) to generate the bar, however, we haven’t implemented the function yet. So, continue to insert the following code:

Widget _buildBar(BuildContext context) {
    return new AppBar(
      elevation: 0.1,
      backgroundColor: appDarkGreyColor,
      centerTitle: true,
      title: _appBarTitle,
      leading: new IconButton(
            icon: _searchIcon
      )
    );
  }

Next, it’s the body. Again, we haven’t implemented the _buildList(context) function. Continue to add these lines of code:

Widget _buildList(BuildContext context) {
    if (!(_searchText.isEmpty)) {
    _filteredRecords.records = new List();
      for (int i = 0; i < _records.records.length; i++) {
        if (_records.records[i].name.toLowerCase().contains(_searchText.toLowerCase())
            || _records.records[i].address.toLowerCase().contains(_searchText.toLowerCase())) {
          _filteredRecords.records.add(_records.records[i]);
        }
      }
    }
 
    return ListView(
      padding: const EdgeInsets.only(top: 20.0),
      children: this._filteredRecords.records.map((data) => _buildListItem(context, data)).toList(),
    );
  }

Here, we handle the mapping of our RecordList data into our ListVew, and also handle any searches performed.

The final piece of our ListView is the UI for each ListViewItem. Let’s create the _buildListItem function:

Widget _buildListItem(BuildContext context, Record record) {
    return Card(
      key: ValueKey(record.name),
      elevation: 8.0,
      margin: new EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
      child: Container(
        decoration: BoxDecoration(color: Color.fromRGBO(64, 75, 96, .9)),
        child: ListTile(
          contentPadding:
          EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
          leading: Container(
              padding: EdgeInsets.only(right: 12.0),
              decoration: new BoxDecoration(
                  border: new Border(
                      right: new BorderSide(width: 1.0, color: Colors.white24))),
              child: Hero(
                  tag: "avatar_" + record.name,
                  child: CircleAvatar(
                    radius: 32,
                    backgroundImage: NetworkImage(record.photo),
                  )
              )
          ),
          title: Text(
            record.name,
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
          subtitle: Row(
            children: [
              new Flexible(
                  child: new Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        RichText(
                          text: TextSpan(
                            text: record.address,
                            style: TextStyle(color: Colors.white),
                          ),
                          maxLines: 3,
                          softWrap: true,
                        )
                      ]))
            ],
          ),
          trailing:
          Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 30.0),
          onTap: () {},
        ),
      ),
    );
  }

This is a long chunky piece of code. We can again break down and digest this in a simpler way:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

After implementing all these, it’s almost ready to run the app and test it out! One last thing to make it work is to handle the onPressed event of the login button. Previously, we haven’t specified anything in the implementation. Now go to LoginPage.dart and change the onPressed event of the loginButton variable to the following:

onPressed: () {
          Navigator.of(context).pushNamed(homePageTag);
        },

That’s it! Hit the run button and try to navigate the app from the login page to the home page!

Adding Search Feature

To allow search capability, we have to enable the text editor’s listener. Insert the code below after the _buildListItem method of the HomePage.dart file:

_HomePageState() {
    _filter.addListener(() {
      if (_filter.text.isEmpty) {
        setState(() {
          _searchText = "";
          _resetRecords();
        });
      } else {
        setState(() {
          _searchText = _filter.text;
        });
      }
    });
  }
 
  void _resetRecords() {
    this._filteredRecords.records = new List();
    for (Record record in _records.records) {
      this._filteredRecords.records.add(record);
    }
  }

The search process starts by tapping the search icon. When the search is triggered, we will perform some UI changes:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

So here is the code you need. Continue to add the following method to handle the search:

void _searchPressed() {
    setState(() {
      if (this._searchIcon.icon == Icons.search) {
        this._searchIcon = new Icon(Icons.close);
        this._appBarTitle = new TextField(
          controller: _filter,
          style: new TextStyle(color: Colors.white),
          decoration: new InputDecoration(
            prefixIcon: new Icon(Icons.search, color: Colors.white),
            fillColor: Colors.white,
            hintText: 'Search by name',
            hintStyle: TextStyle(color: Colors.white),
          ),
        );
      } else {
        this._searchIcon = new Icon(Icons.search);
        this._appBarTitle = new Text(appTitle);
        _filter.clear();
      }
    });
  }

In order to trigger _searchPressed(), add this method in onPressed to _buildBar:

Widget _buildBar(BuildContext context) {
    ...
    icon: _searchIcon,
    onPressed: _searchPressed
    ... 
}  

Now you’re ready to go! Try running the app now and perform some searches! like “Mark”.

Building Contact Details Page

To finish up our Contactly App, let’s build our final Details Page to allow the app to show some more info about a contact. Let’s look at how the final screen looks like first:

It shows the contact’s profile image, its name, address, and phone number. One hidden feature not shown here is to allow user to navigate to an external web browser to view the technology’s website. So let’s get started!

In lib, create a new file called DetailsPage.dart and paste in the following code:

import 'package:flutter/material.dart';
import 'models/Record.dart';
 
// 1
class DetailPage extends StatelessWidget {
  final Record record;
  // 2
  DetailPage({this.record});
 
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text(record.name),
        ),
        body: new ListView(
            children: [
              Hero(
                tag: "avatar_" + record.name,
                child: new Image.network(
                    record.photo
                ),
              ),
              // 3
              GestureDetector(
                   onTap: () {
                     URLLauncher().launchURL(record.url);
                   },
                  child: new Container(
                    padding: const EdgeInsets.all(32.0),
                    child: new Row(
                      children: [
                        // First child in the Row for the name and the
                        // Release date information.
                        new Expanded(
                          // Name and Address are in the same column
                          child: new Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              // Code to create the view for name.
                              new Container(
                                padding: const EdgeInsets.only(bottom: 8.0),
                                child: new Text(
                                  "Name: " + record.name,
                                  style: new TextStyle(
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ),
                              // Code to create the view for address.
                              new Text(
                                "Address: " + record.address,
                                style: new TextStyle(
                                  color: Colors.grey[500],
                                ),
                              ),
                            ],
                          ),
                        ),
                        // Icon to indicate the phone number.
                        new Icon(
                          Icons.phone,
                          color: Colors.red[500],
                        ),
                        new Text(' ${record.contact}'),
                      ],
                    ),
                  )
              ),
            ]
        )
    );
  }
}

Here is what this above code does:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

You should notice a new UI component called GestureDetector. As its name suggests, this widget class is designed to detect touches. When a user touches one of the fields, the app will call URLLauncher().launchURL(record.url) to load the URL in a web browser. This URLLauncher class is not ready yet.

Let’s create a new file called URLLauncher.dart in the helpers directory.

To perform a url launch, we need to install a new package called url-launcher. To do this, we need to update our pubspec.yaml like this:

Here we add a line of configuration to load the url_launcher. After editing, run flutter packages get by hitting the Packages Get button. This is how we install extra packages to increase the capabilities of our app 🙂 Great! You have just gained another skill!

Now go back to URLLauncher.dart, insert the following code to implement the launchURL method:

import 'package:url_launcher/url_launcher.dart';
 
class URLLauncher {
 
  launchURL(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }
 
}

Head back to the DetailsPage.dart file and import the file we just implemented:

import 'helpers/URLLauncher.dart';

Great! The last step is to enable the navigation from HomePage to DetailsPage. Head back to HomePage.dart and edit the onTap: event of the _buildListItem method like this:

Widget _buildListItem(BuildContext context, Record record) {
            ...
          onTap: () {
            Navigator.push(
                context, MaterialPageRoute(builder: (context) => new DetailPage(record: record)));
          },
        ),
      ),
    );
  }  

Also, don’t forget to import the following file in HomePage.dart:

import 'DetailsPage.dart';

Viola! You are done with the app (not just iOS but Android too)! Run it and enjoy your great work 🙂

Conclusion

You have just gone through a very basic tutorial to get you started in developing on Flutter. In my own opinion, Flutter is developed based on the knowledge of popular mobile apps around where we can easily build UI components in just a few lines of codes. While its scalability is still questionable, we can see that Google and it’s community is investing a lot in this framework, and we could possibly forsee a bright future ahead for Flutter, striving past React Native.

You can download the finished project here.