Program from one source to many apps with Flutter

Big Flap

Weather App

The next example app (Listing 2) displays the current weather with the help of two external packages. The current weather data is provided by the OpenWeatherMap online service. You could receive and analyze the weather data from this source in JSON data format, but a Flutter package named weather [6] for OpenWeatherMap simplifies the steps. All you need is an API key, which you can get for free from the OpenWeatherMap homepage. This key authorizes you to retrieve the weather data.

Listing 2

Weather App

001 import 'package:flutter/material.dart';
002 import 'package:weather/weather.dart';
003 import 'package:shared_preferences/shared_preferences.dart';
004
005 void main() {
006   runApp(MyApp());
007 }
008
009 class MyApp extends StatelessWidget {
010   @override
011   Widget build(BuildContext context) {
012     return MaterialApp(
013       title: 'Weather App',
014       home: Scaffold(resizeToAvoidBottomInset: false, appBar: AppBar(title: Text('Weather App')),
015       body: Center(
016           child: Column(
017           children: [
018             Image.asset('assets/images/weather_icon.png'),
019             Text('Current Weather', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),),
020             Flexible(child: MyHomePage())
021         ],),
022       ),),
023     );
024   }
025 }
026
027 class MyHomePage extends StatefulWidget {
028   @override
029   _MyHomePageState createState() => _MyHomePageState();
030 }
031
032 class _MyHomePageState extends State<MyHomePage> {
033   final _city = TextEditingController();
034   final _apiKey = TextEditingController();
035   var _temperature = '';
036   var _weather = '';
037   var _pressure = '';
038   var _windSpeed = '';
039   var _humidity = '';
040   var _lastUpdate = '';
041
042   void _showApiKeyInputDialog() async {
043     final prefs = await SharedPreferences.getInstance();
044     if(prefs.containsKey('apikey')){
045       var _key = prefs.getString('apikey');
046       showDialog(
047           context: context,
048           builder: (BuildContext context) {
049             return AlertDialog(
050               title: Text('Enter API key'),
051               content: TextField(
052                 onChanged: (value) {
053                 },
054                 controller: _apiKey..text = _key.toString(),
055                 decoration: InputDecoration(hintText: 'API key'),
056                 onSubmitted: (value) async {
057                   await prefs.setString('apikey', _apiKey.text);
058                 },
059               ),
060               actions: [TextButton(child: Text('DELETE KEY'), onPressed: () async {
061               await prefs.remove('apikey'); _apiKey.clear(); Navigator.pop(context);}), TextButton(
062                 child: Text("CANCEL"),
063                 onPressed:  () {Navigator.pop(context);},
064               ), TextButton(
065                 child: Text('OK'),
066                 onPressed: () async { await prefs.setString('apikey', _apiKey.text); Navigator.pop(context);},
067               )],
068           );}
069       );
070     }
071     else {
072       showDialog(
073         context: context,
074         builder: (BuildContext context) {
075           return AlertDialog(
076             title: Text('Enter API key'),
077             content: TextField(
078               onChanged: (value) {
079               },
080               controller: _apiKey,
081               decoration: InputDecoration(hintText: 'API key'),
082               onSubmitted: (value) async {
083                 await prefs.setString('apikey', _apiKey.text);
084               },
085             ),
086             actions: [TextButton(child: Text('DELETE KEY'), onPressed: () async {
087             await prefs.remove('apikey'); _apiKey.clear(); Navigator.pop(context);}),
088             TextButton(
089               child: Text("CANCEL"),
090               onPressed:  () {Navigator.pop(context);},
091             ),
092             TextButton(
093               child: Text('OK'),
094               onPressed: () async { await prefs.setString('apikey', _apiKey.text); Navigator.pop(context);},
095             )],
096       );});
097     }
098   }
099
100   void _displayWeather() async {
101     final prefs = await SharedPreferences.getInstance();
102     var _key = prefs.getString('apikey').toString();
103     WeatherFactory wf = new WeatherFactory(_key, language: Language.ENGLISH);
104     Weather w = await wf.currentWeatherByCityName(_city.text);
105     double? celsius = w.temperature!.celsius;
106     setState(() {
107       _temperature = celsius!.round().toString() + ' Degrees Celsius';
108       _weather = w.weatherDescription!;
109       _pressure = w.pressure.toString() + ' hPa';
110       _windSpeed = w.windSpeed.toString() + ' km/h';
111       _humidity = w.humidity.toString() + ' %';
112       _lastUpdate = w.date!.toIso8601String().replaceFirst('T', ' ').replaceRange(w.date!.toIso8601String().lastIndexOf('.')-1, w.date!.toIso8601String().lastIndexOf('0'), '');
113     });
114   }
115
116   @override
117   Widget build(BuildContext context) {
118     return Scaffold(resizeToAvoidBottomInset: false,
119       body: ListView(children: [Container(child: Column(children: [
120         SizedBox(height: 10),
121         TextFormField(decoration: const InputDecoration(hintText: 'Please enter a city', labelText: 'City'), controller: _city,),
122         SizedBox(height: 10),
123         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Temperature: ', style: TextStyle(fontSize: 18),),
124           Text(_temperature.toString(), style: TextStyle(fontSize: 18)),]),
125         SizedBox(height: 10),
126         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Weather: ', style: TextStyle(fontSize: 18),),
127           Text(_weather, style: TextStyle(fontSize: 18)),]),
128         SizedBox(height: 10),
129         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Pressure: ', style: TextStyle(fontSize: 18),),
130           Text(_pressure.toString(), style: TextStyle(fontSize: 18)),]),
131         SizedBox(height: 10),
132         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Wind speed: ', style: TextStyle(fontSize: 18),),
133           Text(_windSpeed, style: TextStyle(fontSize: 18)),]),
134         SizedBox(height: 10),
135         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Humidity: ', style: TextStyle(fontSize: 18),),
136           Text(_humidity, style: TextStyle(fontSize: 18)),]),
137         SizedBox(height: 10),
138         Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Last updated: ', style: TextStyle(fontSize: 18),),
139           Text(_lastUpdate, style: TextStyle(fontSize: 18)),]),
140         SizedBox(height: 10),
141         SizedBox(width: double.infinity, height: 35, child: ElevatedButton(onPressed: _displayWeather, child: const Text('Display Weather')),),
142       ],))],),
143       floatingActionButton: FloatingActionButton.extended(
144         onPressed: _showApiKeyInputDialog, icon: Icon(Icons.add), label: Text('API key'), tooltip: 'Enter API key',)
145     );
146   }
147 }

The second package named shared_preferences [7] stores this API key locally on the target platform, so you do not need reenter the key after each reboot. This very popular package is used a great deal in Flutter development because storing small amounts of data, like a string here, is straightforward. It avoids the use of a full-blown database, which would be technical overkill in this case.

The graphic with the sun at the very top of the sample app's interface (Figure 2) is in PNG format. To insert an image file like this, you need to declare it in pubspec.yaml by specifying the path to the file. Usually, external resources like this are stored in the assets/ folder of the Flutter project. In the flutter section, note the file path to the image file, as shown in Listing 3; then, add the graphic to the UI in the build method of the StatelessWidget, as shown in line 18 of Listing 2. The Center widget wrapper centers the graphic.

Listing 3

Graphical Assets

flutter:
  assets:
    - assets/images/weather_icon.png
Figure 2: The weather app displays the latest weather data.

In line 14, a Scaffold is passed to the home area. It groups an app bar with the text 'Weather App' along with the resizeToAvoidBottomInset option set to false. When the app's graphical keyboard pops up, this means it can overlay the widgets. The text widget in line 19 formats the 'Current Weather' header. The first parameter specifies the desired string, and the second specifies the type and size of the font with the TextStyle widget. In this case, the headline is displayed in 24-point (fontSize) bold (fontWeight).

In line 20, the Flexible widget inserts the StatefulWidget named MyHomePage into the UI with its child attribute. With the Flexible widget, the child widget dynamically resizes. MyHomePage is a StatefulWidget because it defines the area of the graphical interface that can change and is why it inherits the functionality of the StatefulWidget class in line 27.

API Key

Line 42 creates an asynchronous _showApiKeyInputDialog function that is used to query the API key. The underscore at the beginning of the function name means that the function is only visible in its own Dart file, as are variable and class names. The function is called as soon as you press the FloatingActionButton bottom right in the app. Line 143 defines the matching button. The onPressed parameter specifies the function to be called when the button is pressed. In this case, an AlertDialog from the Material library is displayed, and you need to enter the API key from OpenWeatherMap that you created earlier.

As mentioned before, the key is stored locally with shared_preferences; this arrangement works quite well. Line 43 creates an instance of shared_preferences named prefs inside the _showApiKeyInputDialog function, which is an asynchronous action thanks to the await keyword. The string itself is stored in the cache as a key-value pair, much like an associative array. In this example, the apikey string represents the key.

The prefs.containsKey('apikey') method in line 44 checks to see whether a key already exists on the target device. If so, the method reads its value and assigns it to the _key variable. Finally, line 46 calls the dialog with showDialog(). To do this, line 49 returns an AlertDialog, as mentioned earlier, whose TextField then displays the API key.

If no API key resides in the local cache, the else branch displays an identical dialog starting in line 74, but with an empty text box. In both cases, the AlertDialog has three options: DELETE KEY deletes the API key from the cache, CANCEL closes the dialog, and OK saves the entry currently in the text box as the API key. To create these options, three TextButtons below the actions parameters in lines 60 and 86 are defined. When done, Navigator.pop(context) closes the dialog again in each case.

The counterpart to prefs.getString('apikey') for reading the API key is the prefs.setString('apikey', _apiKey.text) method, which stores a string locally. Lines 66 and 94 make use of this to store the API key with shared_preferences. The first parameter specifies the key, and the second names the string to be stored under that key. In this case, the second parameter consists of a TextEditingController that is connected to the text field from the _showApiKeyInputDialog function and reads the API key there. If you want to use DELETE KEY to clear the API key from the cache, this is done with the prefs.remove('apikey') method in lines 61 and 87. The subsequent _apiKey.clear() sets the value of the TextEditingController to empty.

The Weather

Line 100 formulates another asynchronous function named _displayWeather() that displays the weather and is executed when the Display Weather button is pressed. Line 141 adds this ElevatedButton, the name of the button widget in Flutter, to the graphical interface. The second option, child, lets you label the button with a text widget. If the weather data has changed, line 106 uses setState() to assign the new values to the variables and update the text widgets in the interface.

Displaying the current weather requires another instance of SharedPreferences (line 101). The following line again uses the prefs.getString('apikey') method to read the current value in the cache and assign it to the _key variable. In line 103, the WeatherFactory class comes into play for the first time; it is used to retrieve weather data from the OpenWeatherMap online service. WeatherFactory belongs to the weather package imported in line 2. The new keyword instantiates an object of this class named wf. As the first parameter, the constructor expects the API key in the variable _key. The second optional parameter language can be used to define the language for the weather display.

The wf object comes with a method named currentWeatherByCityName, which expects as a parameter the city for which you want the weather displayed by the app. In this case, a TextEditingController again acts as a parameter to read the city specified in the text box in line 121. The TextEditingController variable was initialized in line 33 along with the controller property in line 121. The text property of this controller can be used to read the string currently in the associated text box.

Once the city has been passed as a parameter to the currentWeatherByCityName method, you can use the associated attribute temperature to determine the temperature at this location. As you can see in line 107, the temperature is displayed in degrees Celsius (alternatively, you could display it in Fahrenheit or Kelvin). The w object from line 104 has other attributes, too, such as weatherDescription, which describes the weather, and pressure, which gives the air pressure. Similarly, windSpeed provides the wind speed, humidity the humidity, and date the date and time of the weather measurement. The corresponding variables for each weather property are initialized in lines 35-40.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Program GUIs in Go with Fyne
    In Go, which was originally developed for system programming, graphical user interfaces were not typically necessary. But a relatively new toolkit, Fyne, lets programmers build platform-independent GUIs for Go programs.
  • Haskell framework for the web
    The Yesod web framework includes a Haskell compiler on top of the standard web technologies, such as HTML, JavaScript, and CSS, with aggressive optimization and function checks for correctness.
  • Video: Modeling Climate
  • Modularize websites with Web Components
    Web Components let you define your own HTML tags to restructure monolithic web pages into smaller services and simplify maintenance and servicing.
  • GUI or Text-Based Interface?

    Sys admins are like smokejumpers who parachute into fires, fighting them until they are out, or at least under control. When you jump into the fire, you only have the tools you brought with you.

comments powered by Disqus