Create your first Flutter app, an APOD cross-platform application

Pierre Monier
13 min readFeb 28, 2021

--

Flutter is a cross-platform framework, built by Google to create beautiful and robust applications. It’s written in a relatively new language named Dart, also built by Google. In this tutorial, we’re going to create our first app with flutter. We’re going to create a to-do app or a counter app like every other tutorial out there. We’re going to create an APOD app. If you’re an astronomy fan you already know what I’m talking about. APOD (Astromical Picture Of the Day) is a fantastic website maintained by NASA. Every day, a new astronomical picture is uploaded, followed by a description to explain what is going on.

Our app is going to display these informations, but we’re going to do more than that. We’re going to add an option to set the daily picture as the device wallpaper.

This tutorial requires to have a functional version of Flutter installed. Also, you need an IDE (Android Studio or VsCode) with Flutter and Dart support and a mobile device or a mobile device emulator.

First step we are going to create the default Flutter with the Flutter cli tool. It’s really straightforward, just open a terminal and tap

flutter create apod_app

This will create a counter app. Now you can run the project on a device. Just go in your app with a terminal and tap

flutter run

Now the application is running on your device. It’s a simple example, but it shows up how we build a UI with Flutter. We use widget, it’s the basic of class you are going to use a lot. There is two type of widget, the StatelessWidget and the StatefulWidget. StatelessWidget are immutable, once a StatelessWidget is render by Flutter, it doesn’t change. StatefulWidget on the other hand hold a state object. This object can’t change over time and when it changes, the UI is updated. In the default app, tapping on the button call this function

void _incrementCounter() {
setState(() {
_counter++;
});
}

As you can see, it calls another function named setState. This function tells Flutter that something has changed in this State, which trigger the build method. The build method is the one who display the data. It returns others widget objects.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}

You must use StatelessWidget over StatefullWidget. StatelessWidget are much simpler so they render much faster. Of course, you will be forced to use StatefullWidget in your Flutter journey. But we can get rid of them in a lot of case. We are going to see that in this tutorial.

Now that we have seen the basic of Flutter, let’s get back on our project. We need to access the APOD website data. Thankfully, the NASA has made a sweet API. They also have others very cool API, you can check that out. The APOD API is really, really simple, you just have to call the right endpoint and boom, it gives you nice json data.

We are going to do things the right way, because we want to learn how to use Flutter. So we are going to create two files, a .env file and a .env.dist file at the root of the project. We should add the .env file to our .gitignore file. In the .env file add the following variable

API_URL=https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&thumbs=true

The demo key limit you to 30 requests per IP address per hour and 50 requests per IP address per day. So it’s fine for our tutorial but if you want to create a real application based on this tutorial, you should get a real API key. You can notice that we add a parameter to the URL named “thumbs”. It tells the NASA server to give us thumbnails when the astronomical picture of the day is a … video. It will be useful to detect if the pictures of the day is a video, also it will be necessary to the set has wallpaper feature.

Now we need to tell our Flutter project to use the .env file. Go to your pubspec.yml file and add this line

flutter:
assets:
- .env

You can add all kind of file using this method (Image, Json…). Now we need something to parse our .env file. We are going to use a library named flutter_dotenv. First go check the documentation to see what it’s all about. To install the plugin, add the dependency in the pubspec.yml file.

dependencies:
flutter:
sdk: flutter
flutter_dotenv: ^2.1.0

Next you need to get the new dependency with this command

flutter pub get

Now you have add the flutter_dotenv to your project. Just import the plugin in your main file

import 'package:flutter_dotenv/flutter_dotenv.dart';

Now we can change the main function a bit to parse our .env file access the API URL

void main() async {
await DotEnv().load('.env');
final apiUrl = DotEnv().env['API_URL'];
runApp(MyApp(apiUrl: apiUrl));
}

We also need to change our MyApp widget a bit, add this at the beginning of the class

MyApp({Key key, this.apiUrl}) : super(key: key);
final String apiUrl;

It defines a String attribute named apiUrl, and set this attribute to the value pass in the constructor. You can add this

print(apiUrl);

At the start of the build method, to check the value of the apiUrl. Great, we have access to the right URL, now we can make an HTTP call to get the data ! Before that, just make a little clean up. We don’t need the StatefullWidget, so you can change

MyHomePage extends StatefullWidget

to

MyHomePage extends StatelessWidget

and remove the State property. You can also change the attribute definition to

MyHomePage({Key key, @required this.apiUrl}) : super(key: key);
final String apiUrl;
final title = "APOD WALLPAPER";

And of course pass the apiUrl to the MyHomePage object

home: MyHomePage(apiUrl: apiUrl)

Ok great ! Now we can make an HTTP call. We are going to use another plugin named dio. It’s a high level HTTP library, it’s really the go-to solution to make an HTTP call, very easy to use with some helpers for testing. Remember, to add a plugin to our app, 3 steps. First add the dependency to the pubspect.yml file

dependencies:
flutter:
sdk: flutter
flutter_dotenv: ^2.1.0
dio: ^3.0.10

next

flutter pub get

and finally in our code

import 'package:dio/dio.dart';

Magnificent ! Now we can use this plugin in our app. We can add an attribute to our class, an attribute name _dio. This attribute will be a private member (cause of the _) and will contain a Dio object, the one we are going to use to make HTTP call

final _dio = Dio();

Let's write a function which returns the APOD website data.

Future<void> _getApodData() async {
final json = await _dio.get(apiUrl);
print(json.data);
print(jYou can improve this app ! There is a lot of stuff to do. You can for example add the possibility to see the APOD content of a specific day or let the user set the picture as lock screen wallpaper. This is the GitHub repository, pull request are welcomed :)son.data.runtimeType.toString());
}

Ok so this function is doing three things. First it is awaiting the result of the get method, called on the Dio object. We need to await this result because it’s a Future. In Dart, Future is the Promise concept implementation. It will return a value, and this value might be the wanted result or an error. Next we print our result, and the type of this result.

So _InternalLinkedHashMap is not a very useable type in Dart. We want to do things the right way. That means we should implement a model class which represent the APOD API data. To create a model class in lazy mode, we are going to use this website. It’s a model generator, you pass json data, you get a Dart model. Perfect, let’s do it !

class Apod {
String date;
String explanation;
String mediaType;
String serviceVersion;
String thumbnailUrl;
String title;
String url;

Apod.fromJson(Map<String, dynamic> json) {
date = json['date'];
explanation = json['explanation'];
mediaType = json['media_type'];
serviceVersion = json['service_version'];
thumbnailUrl = json['thumbnail_url'];
title = json['title'];
url = json['url'];
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['date'] = this.date;
data['explanation'] = this.explanation;
data['media_type'] = this.mediaType;
data['service_version'] = this.serviceVersion;
data['thumbnail_url'] = this.thumbnailUrl;
data['title'] = this.title;
data['url'] = this.url;
return data;
}
}

Let’s put this in subdirectory named “models” in a file named apod_model.dart. Now we just need to change the _getApodData method

Future<Apod> _getApodData() async {
final json = await _dio.get(apiUrl);

final data = jsonDecode(json.toString());
final apod = Apod.fromJson(Map<String, dynamic>.from(data));

_apod = apod;
return apod;
}

We use a method to decode the json data (jsonDecode). We must import a package in order to use it. It’s a Dart native package. At the top of your main.dart file, add

import 'dart:convert';

You can notice that we are creating a Map with the static from method. We are casting our data to avoid problems. You can also notice the _apod class attribute, we are setting the value of this attribute to the value of our created Apod object. Spoiler, but we are going to use it later :), just add another attribute to the MyHomePage class

Apod _apod;

At this stage we have a perfect useable model, now it’s time to use it ! But there is a problem. We are getting data in an asynchronous way. So we don’t have the data when the app is launching. So how to we update our UI ? Use a state ! Ok, it might work but remember, we must use state as a last resort. The goal of state is to update the UI based on user interaction. In our case, the user doesn’t have any interaction, so we must not use state.

There is a lot of very helpful widget in Flutter and guess what, there is one to solve our problem. It’s called FutureBuilder and it takes two parameters. One named future, which is a function returning Future. The second parameter is builder, a callback function called to build the UI, depending on the state of the Future.

Let’s right our new build method !

Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: SingleChildScrollView(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FutureBuilder<Apod>(
future: _getApodData(),
builder: (BuildContext context, AsyncSnapshot<Apod> snapshot) {
Widget children;
if (snapshot.hasData) {
children = _formatData(snapshot: snapshot.data);
} else if (snapshot.hasError) {
children = _formatError();
} else {
children = _formatLoading();
}

return children;
},
)
],
),
)),
);
}

Ok, what’s going on here ? First we render a Scaffold, this widget is the base of what to draw on the screen. Next we have a Center widget use to … center his child component. SingleChildScrollView allow the user to scroll the screen. It’s useful here because the explanation text might overflow the screen. Next we have Column component which takes a children property. This widget is useful when you want to render more than one child. In our case it’s useless yeah, because we only have the FutureBuilder has a child. We can change it with a Container widget for example. Finally, we have the FutureBuilder. If you just copy and paste the code your IDE should be warning you because he doesn’t know _formatData, _formatError and _formatLoading method. These functions render widget, depending on the state of the Future, represented by the AsyncSnapshot<Apod> object. Let’s be negative, we are going to right the _formatError function. This function render a widget to tell the user that an error has occurred.

Container _formatError() {
return Container(child: Icon(Icons.signal_wifi_off ));
}

Really simple it renders an Icon who tells the user, there is no network connection. Next, _formatLoading function render a widget to tell the user: Please wait, I’m loading data.

Container _formatLoading() {
return Container(child: CircularProgressIndicator());
}

Again really simple ! The CircularProgressIndicator is a built-in Flutter widget, very useful. We can customize it has we want. And last but certainly not least, the _formatData function

Column _formatData({@required Apod snapshot}) {
return Column(children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text.rich(
TextSpan(
text: snapshot.date + ": ",
children: <TextSpan>[
TextSpan(text: snapshot.title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24)),
],
),
),
),
_getMainContent(apod: snapshot),
Padding(padding: const EdgeInsets.all(20),
child: Text(snapshot.explanation))
]);
}

Ok, you can notice the @required annotation and the in braces {} parameter. In braces implement named parameter. Now we can do square(x: x). But the default is named parameter are optional. So we add the required annotation to solve this issue.

Next we are rendering Padding widget, it’s a container who add padding to his child. Next we have a _getMainContent function, we will see that in a minute. And finally a basic text, wrapped inside a Padding widget. Notice how we use the Text.rich method to render a text compose by two TextSpan

The main job happened in the _getMainContent method. Let’s see what going on out there

Widget _getMainContent({@required Apod apod}) {
return apod.isImage() ? Image.network(
apod.url,
loadingBuilder: (context, child, progress) {
return progress == null ? child : CircularProgressIndicator(
value: progress.cumulativeBytesLoaded / progress.expectedTotalBytes,
);
}
) : YoutubePlayer(controller: _getVideoController(videoId: YoutubePlayer.convertUrlToId(apod.url)));
}

We use a specific method to render the “main content” which is the picture or video of the day. We determine if it’s a video or an image with a method implemented on the model

bool isImage() {
return (thumbnailUrl == null);
}

Simple, if we have a thumbnailUrl, that means it is a video. In this case we render a YoutubePlayer. It’s another plugin, now you now how to install plugin in Flutter, if you have some doubts, just follow the documentation. A YoutubePlayer widget need to have a YoutubePlayerController widget. It’s used to get the video, add some option (autoplay, mute…). We render the controller with another method named _getVideoController, and yeah I like to split my code

YoutubePlayerController _getVideoController({@required String videoId}) {
return YoutubePlayerController(
initialVideoId: videoId,
flags: YoutubePlayerFlags(
autoPlay: false,
),
);
}

Very simple, the hard job is done before by the YoutubePlayer plugin. The convertUrlToId method which take our apod.url and convert it to a videoId.

If the picture of the day is a real picture then we return an Image. We use the built-in Flutter widget named Image. We use a static method named network to get the image based on an URL. Next we add a loadingBuilder function to render a nice CircularProgressIndicator widget while getting the image data.

loadingBuilder: (context, child, progress) {
return progress == null ? child : CircularProgressIndicator(
value: progress.cumulativeBytesLoaded / progress.expectedTotalBytes,
);
}

Something useful here is the parameter named progress. It’s an ImageChunkEvent object. This object represents progress notifications while an image is being loaded. If our object is null, then it means that the image is fully load. In this case the loadingBuilder function return the child (the image). If it’s still loading, we use our ImageChunkEvent object to give value about the loading to our CircularProgressIndicator object. By specifying the value to progress.cumulativeBytesLoaded / progress.expectedTotalBytes, we tell the user when our Image will be loaded.

We are almost finish. The last step is to implement the setHasWallpaper feature. We will need three very useful plugins. First fluttertoast, simplify how our app is displaying Toast. We are also going to use flutter_cache_manager to download the picture of the day on disk. Finally wallpaper_manager, a plugin to set picture has wallpaper.

Now we are going to update our build method to render a FloatingActionButton widget.

Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: SingleChildScrollView(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FutureBuilder<Apod>(
future: _getApodData(),
builder: (BuildContext context, AsyncSnapshot<Apod> snapshot) {
Widget children;
if (snapshot.hasData) {
children = _formatData(snapshot: snapshot.data);
} else if (snapshot.hasError) {
children = _formatError();
} else {
children = _formatLoading();
}

return children;
},
)
],
),
)),
floatingActionButton: FloatingActionButton(
onPressed: () => _apod != null ? _setHasWallpaper(context) : _showToast("Wait for data"),
child: Icon(Icons.wallpaper),
),
);
}

floatingActionButton is a Scaffold property, used to display a FAB. Notice how simple we do to add an Icon to this button. We bind the onPressed event to a function executing a specific function, depending on our _apod class attribute. If the MyHomePage object hasn’t fetch the APOD API wet we just display a toast to the user with this method

void _showToast(String result) {
Fluttertoast.showToast(
msg: result,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.grey,
textColor: Colors.white,
fontSize: 16.0
);
}

Here we use the fluttertoast plugin. It’s very simple and straight forward. And finally the _setHasWallpaper function.

Future<void> _setHasWallpaper(BuildContext context) async {
final file = await DefaultCacheManager().getSingleFile(_apod.getWallpaperUrl());
final String result = await WallpaperManager.setWallpaperFromFile(file.path, WallpaperManager.HOME_SCREEN);

_showToast(result);
}

So first we are downloading the APOD picture with the flutter_cache_manager plugin. Next we set the downloaded file as wallpaper thanks to the wallpaper_manager plugin. Very simple !

With all this you have now a good overview of how to use Flutter plus a cool app. If you want to compile this code to an iOS application you can ! Just make sure to follow the iOS set up of the youtube_player_flutter plugin.

You can improve this app ! There is a lot of stuff to do. You can for example add the possibility to see the APOD content of a specific day or let the user set the picture as lock screen wallpaper. This is the GitHub repository, pull request are welcomed :)

--

--

Pierre Monier

Developer interested in a lot of subjects. Like to create stuff with code and love to learn new things every day.