Flutter Web and Desktop

Believe it or not, Flutter originally started as a fork of Chrome with a simple question – how fast can the web go if you don’t worry about maintaining over 20 years of technical debt? The answer to that question is the blazingly fast mobile framework that we love. Now Flutter is returning to its roots and once again running on the web. There are many other places where people are experimenting with Flutter, including Desktop, ChromeOS, and even the Internet of Things (IoT). Eventually, it will get to a point where if your device has a screen, it will be able to run Flutter.

Since Flutter 2.0, developers can create mobile, web, and desktop apps: this means that you can create apps that work on iOS, Android, the web, Windows, macOS, or Linux with the same code base.

While you could use exactly the same design and code for all operating systems and devices, there are cases where this might not be the optimal approach: an app screen designed for a smartphone might not be ideal for a large desktop, and not all packages are compatible with all systems. Also, setting up permissions depends on the target destination of your app.
When creating a desktop app, you need to develop on the same platform as your target app: you need a Mac to develop for macOS, a Windows PC if you target Windows, and a Linux PC when targeting Linux.

This tutorial will focus on how to develop and run your apps on web and desktop devices, and how to create responsive apps based on the screen size where your app is running. 

In particular, we will cover the following topics:

  • Creating a responsive app leveraging Flutter Web
  • Running your app on macOS
  • Running your app on Windows
  • Deploying a Flutter website
  • Responding to mouse events in Flutter Desktop
  • Interacting with desktop menus

By the end of this tutorial, you will know how to design your apps not only for mobile devices but also for the web and desktop.

Creating a responsive app leveraging Flutter Web

Running a web app with Flutter might be as simple as running the flutter run -d chrome command on your Terminal. In this recipe, you will also see how to make your layout responsive, and build your app so that it can be later published to any web server. You will also see how to solve a CORS issue when loading images.

In this recipe, you will build an app that retrieves data from the Google Books API, and shows text and images. After running it on your mobile emulator or device, you will then make it responsive, so that when the screen is large, the books will be shown in two columns instead of one.

Getting ready

There are no specific requirements for this recipe, but in order to debug your Flutter apps for the web, you should have the Chrome browser installed. If you are developing on Windows, Edge will work as well. 

How to do it…

In order to create a responsive app that also targets the web, follow these steps:

  1. Create a new Flutter project, and call it books_universal.
  2. In the project’s pubspec.yaml file, in the dependencies section, add the latest version of the http package:
 http: ^0.13.0
  1. In the lib directory of your project, create three new directories, called modelsdata, and screens.
  2. In the models directory, create a new file, called book.dart.
  3. In the book.dart file, create a class called Book, with the fields specified here:
 class Book {
String id;
String title;
String authors;
String thumbnail;
String description;
}
  1. In the Book class, create a constructor that sets all the fields:
 Book(this.id, this.title, this.authors, this.thumbnail, 
this.description);
  1. Create a named factory constructor, called fromJson, that takes a Map and returns a Book, as shown in the code sample:
 factory Book.fromJson(Map<String, dynamic> parsedJson) {
final String id = parsedJson['id'];
final String title = parsedJson['volumeInfo']['title'];
String image = parsedJson['volumeInfo']['imageLinks'] == null
? '' : parsedJson['volumeInfo']['imageLinks']['thumbnail'];
image.replaceAll('http://', 'https://');
final String authors = (parsedJson['volumeInfo']['authors'] ==
null) ? '' : parsedJson['volumeInfo']['authors'].toString();
final String description =
(parsedJson['volumeInfo']['description'] == null)
? ''
: parsedJson['volumeInfo']['description'];
return Book(id, title, authors, image, description);
}
  1. In the data directory, create a new file and call it http_helper.dart.
  2. At the top of the http_helper file, add the required import statements, as shown here:
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart';
import '../models/book.dart';
  1. Under the import statements, add a class and call it HttpHelper:
 class HttpHelper {}
  1. In the HttpHelper class, add the strings and Map required to build the Uri object that will connect to the Google Books API:
final String authority = 'www.googleapis.com';
final String path = '/books/v1/volumes';
Map<String, dynamic> params = {'q':'flutter dart', 'maxResults': '40', };
  1. Still in the HttpHelper class, create a new asynchronous method, called getFlutterBooks, that returns a Future of a List of Book objects, as shown in the following code:
 Future<List<Book>> getFlutterBooks() async {
Uri uri = Uri.https(authority, path, params);
Response result = await http.get(uri);
if (result.statusCode == 200) {
final jsonResponse = json.decode(result.body);
final booksMap = jsonResponse['items'];
List<Book> books = booksMap.map<Book>((i) =>
Book.fromJson(i)).toList();
return books;
} else {
return [];
}
}
  1. In the screens directory, create a new file called book_list_screen.dart.
  2. At the top of the book_list_screen.dart file, add the required imports:
 import 'package:flutter/material.dart';
import '../models/book.dart';
import '../data/http_helper.dart';
  1. Under the import statements, create a new stateful widget, and call it BookListScreen:
 class BookListScreen extends StatefulWidget {
@override
_BookListScreenState createState() => _BookListScreenState();
}
class _BookListScreenState extends State<BookListScreen> {
@override
Widget build(BuildContext context) {
return Container();
}
}
  1. At the top of the BookListScreenState class, create two variables: a List of books, called books, and a Boolean, called isLargeScreen:
 List<Book> books = [];
bool isLargeScreen;
  1. In the BookListScreenState class, override the initState method, and there call the getFlutterBooks method to set the value of the books variable, as shown here:
 @override
void initState() {
HttpHelper helper = HttpHelper();
helper.getBooks('flutter').then((List<Book> value) {
setState(() {
books = value;
});
});
super.initState();
}
  1. At the top of the build method, use the MediaQuery class to read the width of the current device, and based on its value, set the isLargeScreen Boolean to true when the number of device-independent pixels is higher than 600:
 if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}
  1. Still in the build method, instead of returning a Container, return a Scaffold, that in its body contains a responsive GridView. Based on the value of the isLargeScreen variable, set the number of columns to 2 or 1, and childAspectRatio to 8 or 5, as shown in the following code sample:
 return Scaffold(
appBar: AppBar(title: Text('Flutter Books')),
body: GridView.count(
childAspectRatio: isLargeScreen ? 8 : 5,
crossAxisCount: isLargeScreen ? 2 : 1,
children: List.generate(books.length, (index) {
return ListTile(
title: Text(books[index].title),
subtitle: Text(books[index].authors),
leading: CircleAvatar(
backgroundImage: (books[index].thumbnail) == '' ? null :
NetworkImage(books[index].thumbnail),
),
);
})));
}
  1. Edit the main.dart file, so that it calls the BookListScreen widget:
 import 'package:flutter/material.dart';
import 'screens/book_list_screen.dart';

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BookListScreen(),
);
}}
  1. Run the app on your mobile device. You should see an app screen similar to the screenshot:
  1. Stop the app, then in the Terminal window in your project’s directory, run this command:
 flutter run -d chrome

Note that the app is running on your web browser, and showing two columns, but the images are not showing:

  1. Close the browser window, or press Ctrl + C on the Terminal to stop the debug.
  2. In the Terminal, run the following command:
 flutter run -d chrome --web-renderer html
  1. This time, note that the images are showing correctly:

How it works…

When you retrieve data from a web API, the first step is usually to define which fields you are interested in. In this case, we only want to show a selection of the available data that Google Books publishes: in particular the ID, title, authors, thumbnail, and a description of each book.

When a web service returns data in JSON format, in Dart and Flutter you can treat it as a Map. The name of the fields are strings, and the values can be any data type: that’s why we created a constructor that takes a Map<String, dynamic> and returns a Book.

In the Book.fromJson constructor, based on the JSON format returned by the service, you read the relevant data. Note the instructions:

 String image = parsedJson['volumeInfo']['imageLinks'] == null
? ''
: parsedJson['volumeInfo']['imageLinks']['thumbnail'];
image.replaceAll('http://', 'https://');

When you read data returned by a web service, it’s always recommended to check whether the data you expect is available or it’s null.


In the case of the imagelinks field, at this time, Google Books returns an HTTP address: as this is not enabled by default on several platforms, including iOS and Android, it may be a good idea to change the address from http:// to https://. This is the purpose of the replaceAll method called on the image string.
You can also enable retrieving data through an http connection: the procedure depends on your target system. See https://flutter.dev/docs/release/breaking-changes/network-policy-ios-android for more information.

Another breaking change happened in the http package from version 0.13: the http.get method now requires a Uri object instead of a string. You build a Uri passing the authority (domain name) and the path. If you want to add one or more parameters, you add them with a Map:

 final String authority = 'www.googleapis.com';
final String path = '/books/v1/volumes';
Map<String, dynamic> params = {'q':'flutter dart', 'maxResults': '40', };

When building a Uriauthority and path are required; the parameters are optional:

 Uri uri = Uri.https(authority, path, params);

There are several strategies to make your app responsive: one of them is using the MediaQuery class. In particular, MediaQuery.of(context).size allows you to retrieve the device screen size in pixels. These are actually logical (or device-independent) pixels. Most devices use a pixelRatio to make images and graphics uniform across devices.If you want to know the number of physical pixels in your device, you can multiply the size and the pixelRatio, like this:

MediaQuery.of(context).size.width * MediaQuery.of(context).devicePixelRatio .

The MediaQuery.of(context).size property returns a Size object: this contains a width and a height. In our example, we only need to get the device width. We consider a screen “large” when it has a width higher than 600 logical pixels. In this case, we set the isLargeScreen Boolean value to true. This is an arbitrary measure and may depend on the objects you want to show on your designs, or on the orientation of the device (portrait or landscape).

Based on the value of the isLargeScreen Boolean value, we leverage the GridView childAspectRatio and crossAxisCount:

 childAspectRatio: isLargeScreen ? 8 : 5,
crossAxisCount: isLargeScreen ? 2 : 1,

By default, each box in a GridView is a square with the same height and width. By changing the childAspectRatio, you can specify a different aspect ratio. For example, the width of the child will be 8 when the screen is large, and 5 when it’s not. The crossAxisCount value in this example sets the number of columns in the GridView.

Another very interesting aspect of this recipe is the last part, where you run the app in your Chrome browser with this instruction:

flutter run -d chrome

The –d option allows specifying which device you want to run your app on. In this case, the web browser. But if you run your app with this option, the images in the GridView are not showing. The solution is running the app with this command:

 flutter run -d chrome --web-renderer html

Before it can be displayed on a browser, your app must be transformed (or rendered) in a language compatible with your browser. Flutter uses two different web renderers: HTML and CanvasKit.

The default renderer on desktop and web is CanvasKit, which uses WebGL to display the UI. This requires access to the pixels of the images you want to show, while the HTML renderer uses the <img> HTML element to show images.
When you try to show the images in these recipes with the default web renderer, you encounter a CORS error.CORS stands for Cross-Origin Resource Sharing. For security reasons, browsers block JavaScript requests that originate from an origin different from the destination. For example, if you want to load a resource from http://www.thirdpartydomain.com, and the origin of the request is http://www.mydomain.com, the request will be automatically blocked. 

When you use an <IMG> element, cross-origin requests are allowed.

See also…

While for simple results, parsing JSON manually is an option, when your classes become more complex, some automation will greatly help you. The json_serializable package is available at https://pub.dev/packages/json_serializable.

For a thorough overview of the differences between the two web renderers and when to use them, see https://flutter.dev/docs/development/tools/web-renderers
If you are interested in how logical pixels work, and how they are different from physical pixels, see https://material.io/design/layout/pixel-density.html.

Running your app on macOS

With version 2, desktop support has been added to the already rich Flutter framework. This means that you can compile Flutter projects to native macOS, Windows, and Linux apps. 

In this recipe, you will see how to run your app on a Mac, and solve a specific permission issue that prevents your code from retrieving data from the web.

Getting ready

Before following this recipe, you should have completed the app in the previous recipe: Creating a responsive app leveraging Flutter Web.

In order to develop for macOS with Flutter, you should also have Xcode and CocoaPods installed on your Mac.

How to do it…

In order to run the app you have built on your Mac, implement the following steps: 

  1. In the Terminal, get to the project you have completed in the previous recipe, and run this command:
 flutter config –enable-macos-desktop
  1. In the same Terminal window, run this command:
 flutter devices
  1. In the list of devices returned by the command, note that macOS (desktop) is now showing among the available devices.

If you don’t see the macOS (desktop) device in the list, try closing and reopening your editor and the Terminal, and then run the flutter devices command again.

  1. In the Terminal, in order to create the macOS app run the command (the dot . is part of the command): 
 flutter create .
  1. Still in the Terminal window, run this command:
 flutter run –d macos
  1. Note that the app is running, but the books and images are not showing on the screen:
  1. Run the app in debug mode from your editor, choosing macos as the device, and note that you get a SocketException (Operation not permitted) error as shown in the screenshot:
  1. Open the macos/Runner/DebugProfile.entitlements file in your project and add this key:
 <key>com.apple.security.network.client</key>
<true/>
  1. Open the macos/Runner/Release.entitlements file and add the same key you added in the DebugProfile.entitlements file.
  2. Run the app again, and note that the books’ data and images are showing correctly.

How it works…

Before running your app on a desktop, you should enable your platform. In this recipe, you enabled macOS with this command:

flutter config –enable-macos-desktop

You have two options in order to run your app on a specific device: one is using the Flutter CLI, and specifying the device where you want to run your app, as with this instruction:

flutter run –d macos

The other way is choosing the device from your editor. With VS Code, you find your devices in the bottom-right corner of the screen. In Android Studio (and IntelliJ Idea), you find it at the top-right corner of the screen.

As with Android and iOS, an app build for macOS requires specific permissions that must be declared before running the app: these permissions are called entitlements in macOS. In a Flutter project, you will find two files for the entitlements: one for development and debugging, called DebugProfile.entitlements, and another for release, called Release.entitlements. In these two files ,you should add the entitlements that are required for your app. In the example in this recipe, a client connection is required, so you just need to add the network client entitlement with the key:

<key>com.apple.security.network.client</key>
<true/>

Even if you can debug your app by just adding your entitlements in the DebugProfile.entitlements file, I recommend you always add them in the Release.entitlements file as well, so that when you actually publish your app, you do not need to copy the entitlements there.

See also 

For a full and updated list of the desktop support capabilities of Flutter, see https://flutter.dev/desktop. If you later want to publish your app to the Mac App Store, see https://developer.apple.com/macos/submit/

Running your app on Windows

Most desktop computers run on Windows, and that’s a fact. Being able to deploy an app built with Flutter to the most widespread desktop operating system is a huge add-on to the Flutter framework.

In this recipe, you will see how to run your apps on Windows.   

Getting ready…

Before following up on this recipe, you should have completed the app in the first recipe in this tutorial: Creating a responsive app leveraging Flutter Web.

In order to develop for Windows with Flutter, you should also have the full version of Visual Studio (not Visual Studio Code) with the Desktop Development with C++ workload installed on your Windows PC.

You can download Visual Studio for free at https://visualstudio.microsoft.com.

How to do it…

In order to run the app that you built in the previous recipe on a Windows Desktop, take the following steps:

  1. In a Command Prompt window, get to the project you completed in the first recipe in this tutorial, and run this command:
 flutter config -–enable-windows-desktop
  1. In the same window, run this command:
 flutter devices
  1. In the list of devices returned by the command, note that Windows (desktop) is now showing as a device.
  2. Run the flutter doctor command, and note that Visual Studio – develop for Windows is showing, as shown in the screenshot:

If you don’t see the Windows (desktop) device in the list, try closing and reopening your Command Prompt, then run the flutter devices command again.

  1. In Command Prompt, in order to create the Windows app, run this command:
 flutter create .
  1. Run the app with your editor, or by calling this command:
 flutter run –d Windows
  1. Note that the app runs correctly, and the books and images are shown on the screen.

How it works…

Before running your app on a desktop, you should enable your platform. In this recipe, this was performed with this command:

flutter config –enable-windows-desktop

You have two options in order to run your app on a specific device: one is using the Flutter CLI, and specifying the device where you want to run your app, as with this instruction:

flutter run –d windows

The other way is choosing the device from your editor. With VS Code, you find your devices in the bottom-right corner of the screen. In Android Studio (and IntelliJ IDEA), you find it at the top-right corner of the screen.

Different from what happens on a Mac, in the Windows client, HTTP connections are currently enabled by default.

See also…

Visual Studio is a full IDE available for Windows and macOS: it allows the development of frontend and backend applications and supports most modern languages. For more information, have a look at https://visualstudio.microsoft.com

Deploying a Flutter website

Once you build your Flutter app for the web, you can deploy it to any web server. One of your options is using Firebase as your hosting platform.

In this recipe, you will deploy your project as a website to Firebase.

Getting ready

Before following up on this recipe, you should have completed the first recipe in this tutorial: Creating a responsive app leveraging Flutter Web

How to do it…

In order to deploy your web app to the Firebase hosting platform, please take the following steps: 

  1. Open a Terminal window and move it to your project folder.
  2. Type the following command:
 flutter build web --web-renderer html
  1. Open your browser and go to the Firebase console at the address https://console.firebase.google.com.
  2. Create a new Firebase project (you can also use an existing one).
  1. Download the Firebase CLI. You will find the link for your OS at https://firebase.google.com/docs/cli.
  2. In your Terminal, type the following command:
 firebase login
  1. From the browser window that asks for your login, confirm your credentials and allow the required permissions. At the end of the process, note the success message in your browser:
  1. In your terminal, note the success message: ✔ Success! Logged in as [yourusername].
  2. Type this command:
 firebase init
  1. When prompted, choose the hosting option.
  2. When prompted, choose the Use an existing project option and choose the project you have created above.
  3. When prompted, type build/web to choose the files that will be deployed.
  4. When prompted, answer “no” when asked whether you want to overwrite the index.html file.
  5. When prompted, confirm that you want to configure a single-page application.
  6. When prompted, choose whether you want to automatically deploy your changes to GitHub. A screenshot of the full configuration process is shown here:
  1. In your Terminal, type the following:
 firebase deploy
  1. At the end of the process, copy the URL of your project and paste it into a web browser (usually https://[yourappname].web.app). Your app has been published!

How it works…

When using Flutter, the web is just another target for your apps. When you use the following command:

 flutter build web

what happens is that the Dart code gets compiled to JavaScript and then minified for use in production. After that, your source can be deployed to any web server, including Firebase. As you saw in the first recipe in this tutorial, you can also choose a web-renderer to suit your needs.

Flutter developers recommend using Flutter for the web when you want to build progressive web applications, single-page applications, or when you need to port mobile apps to the web. It is not recommended for static text-based HTML content, even though Flutter fully supports this scenario as well.

The Firebase Command Line Interface makes publishing a web app with Flutter extremely easy. Here are the steps that you should take, and you followed in this recipe:

  • You create a Firebase project.
  • You install the Firebase CLI (only once for each developing machine).
  • You run firebase login to log in to your Firebase account.
  • You run firebase init. For this step, you need to provide Firebase with some information, including your target project and the position of your files that will be published.

Tip: make sure you answer “no” when asked whether you want to overwrite the index.html file, otherwise you’ll have to build your web app again.

Once you have initialized your Firebase project, publishing it just requires typing the following:

firebase deploy

Unless you set up a full domain, your web address will be a third-level domain, like yourapp.web.app.

Publishing with an FTP client is just as easy: you only need to copy the files in the build/web folder of your project to your web server. As the Dart code is compiled to JavaScript, no further setup is needed on your target server.

See also…

Another common option to publish your Flutter web apps is leveraging GitHub pages. For a detailed guide of the steps required to deploy your Flutter web app to GitHub pages, see https://pahlevikun.medium.com/compiling-and-deploying-your-flutter-web-app-to-github-pages-be4aeb16542f

If you want to learn more about GitHub pages themselves, see https://pages.github.com/

Responding to mouse events in Flutter Desktop

While with mobile devices, users generally interact with your apps through touch gestures, with bigger devices they may use a mouse, and from a developer perspective, mouse input is different than touch.

In this recipe, you will learn how to respond to common mouse gestures such as click and right-click. In particular, you will make the user select and deselect items in a GridView, and change the color of the background to give some feedback to your users.

Getting ready

How to do it

In order to respond to mouse gestures within your app, please follow the next steps:

  1. Open the book_list_screen.dart file in your project.
  2. At the top of the _BookListScreenState class, add a new List of Color, called bgColors, and set it to an empty List:
 List<Color> bgColors = [];
  1. In the initState method, in the then callback of the getFlutterBook method, add a for cycle that adds a new color (white) for each item in the List of books that was retrieved by the method:
 helper.getBooks('flutter').then((List<Book> value) {
int i;
for (i = 0; i < value.length; i++) {
bgColors.add(Colors.white);
}
  1. At the bottom of the _BookListScreenState class, add a new method, called setColor, that takes a Color and an integer, called index, and sets the value of the bgColors list at the index position to the color that was passed:
void setColor(Color color, int index) {
setState(() {
bgColors[index] = color;
});
}
  1. In the build method of the _BookListScreenState class, in the Gridview widget, wrap the ListTile in a Container widget, and the Container itself in a GestureDetector widget, as shown here:
 body: GridView.count(
childAspectRatio: isLargeScreen ? 8 : 5,
crossAxisCount: isLargeScreen ? 2 : 1,
children: List.generate(books.length, (index) {
return GestureDetector(
child: Container(
child: ListTile(
[...]
  1. In the Container, set the color property based on the value of the bgColors list at the index position:
 color: bgColors.length > 0 ? bgColors[index] : Colors.white,
  1. In the GestureDetector widget, add the callbacks for the onTaponLongPress, and onSecondaryTap events, as shown here:
 onTap: () => setColor(Colors.lightBlue, index),
onSecondaryTap: () => setColor(Colors.white, index),
onLongPress: () => setColor(Colors.white, index),
  1. Run the app on your browser or desktop: left-click with your mouse on some of the items in the grid, and note that the background color of the item becomes blue as shown in the screenshot:
  1. Right-click on one or more of the items that you selected previously. The background color will get back to white.
  2. Long-press with the left button of the mouse on one of the items that you selected previously. The background color will go back to white.

How it works…

When you design an app that works both on mobile and on desktop, you should take into account the fact that some gestures are platform-specific. For instance, you cannot right-click with a mouse on a mobile device, but you can long-press.

In this recipe, you used the GestureDetector widget to select/deselect items in a GridView. You can use a GestureDetector both for touch screen gestures, such as swipes and long-presses, and for mouse gestures, such as right-click and scroll. 

To select items and add a light-blue background color, you used an onTap event:

onTap: () => setColor(Colors.lightBlue, index), 

onTap gets called both when the user taps with a finger on a touch screen and when they click on the main button of a mouse or stylus or any other pointing device. This is an example of a callback that works both on mobile and desktop.

To deselect an item, you used both onSecondaryTap and onLongPress:

onSecondaryTap: () => setColor(Colors.white, index),
onLongPress: () => setColor(Colors.white, index),

In this case, you dealt differently with desktop and mobile: the “secondary tap” is mainly available for web and desktop apps that run on a device with an external pointing tool, such as a mouse. This will typically be a mouse right-click, or a secondary button press on a stylus or a graphic tablet.

onLongPress is mainly targeted at mobile devices or touch screens that don’t have a secondary button: it is triggered when the user keeps pressing an item for a “long” period of time (the time depends on the device, but it’s usually 1 second or longer).

In order to keep track of the selected items, we used a List called bgColors, which for each item in the list contains its corresponding color (light blue or white). As soon as the screen loads, the List gets filled with the white color for all the items, as nothing is selected at the beginning:

helper.getFlutterBooks().then((List<Book> value) {
int i;
for (i = 0; i < value.length; i++) {
bgColors.add(Colors.white);
}

When the user taps/clicks on an item, its color changes to blue, with the setColor method, which takes the new color and the position of the item that should change its color:

void setColor(Color color, int index) { 
setState(() {
gbColors[index] = color; });
}

In this way, you leveraged a GestureDetector widget to respond to both touch and mouse events. 

See also 

The GestureDetector widget has several properties that help you respond to any kind of event. For a full guide on GestureDetector, have a look at https://api.flutter.dev/flutter/widgets/GestureDetector-class.html.  

Interacting with desktop menus

Desktop apps have menus that do not exist on mobile form factors. Typically, you expect to see the important actions of a desktop app in its top menu.

In this recipe, you will learn how to add a menu that works on Windows and macOS. 

Getting ready…

Depending on where you want to run your app,

  • For Windows: Running your app on Windows
  • For macOS: Running your app on macOS

How to do it…

In order to add a menu to your desktop app, follow the next steps:

  1. In your project, open the pubspec.yaml file and add the following dependency:
 menubar:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/menubar
  1. In the http_helper.dart file in the data folder, edit the getFlutterBooks method, so that it becomes a more generic getBooks method that takes a String as a parameter, and move the params declaration inside the method, as shown here:
class HttpHelper {
final String authority = 'www.googleapis.com';
final String path = '/books/v1/volumes';
Future<List<Book>> getBooks(String query) async {
Map<String, dynamic> params = {'q':query, 'maxResults': '40', };
...
  1. At the top of the book_list_screen.dart file, add the menubar import:
 import 'package:menubar/menubar.dart';
  1. Move the HttpHelper helper declaration to the top of the _BookListScreenState class, and in the initState method, set helper to be a new instance of HttpHelper:
 class _BookListScreenState extends State<BookListScreen> {
List<Book> books = [];
bool isLargeScreen;
HttpHelper helper;
@override
void initState() {
helper = HttpHelper();
[...]
  1. At the bottom of the _BookListScreenState class, add a new method, called updateBooks, that takes a String as a parameter, calls the getBooks method, and updates the books list:
 updateBooks(String key) {
helper.getBooks(key).then((List<Book> value) {
setState(() {
books = value;
});
});
}
  1. At the bottom of the _BookListScreenState class, add another new method, called addMenuBar, that adds three search terms as menu items in the menu bar, as shown here:
 void addMenuBar() {
setApplicationMenu([
Submenu(label: 'Search Keys', children: [
MenuItem(
label: 'Flutter',
enabled: true,
onClicked: () => updateBooks('flutter')),
MenuDivider(),
MenuItem(
label: 'C#',
enabled: true,
onClicked: () => updateBooks('c#')),
MenuDivider(),
MenuItem(
label: 'JavaScript',
enabled: true,
onClicked: () => updateBooks('javascript')),
])
]);
}
  1. In the initState method, call the addMenuBar method and update the call to the helper method so that it gets Flutter books as soon as the screen loads. The updated initState method should look like the following code:
 @override
void initState() {
addMenuBar();
helper = HttpHelper();
helper.getBooks('flutter').then((List<Book> value) {
setState(() {
books = value;
});
});
super.initState();
}
  1. Run the app. In the menu bar, you should see a new menu, called Search Keys. If you click on it, you should see the three search keys you set previously. The screenshot shows the menus as they appear on a Mac:

How it works…

At the time of writing, the menubar plugin is still in experimental mode, which means it will probably be included in Flutter or in an official package soon. Meanwhile, you can use it through its GitHub URL. This is why in the pubspec.yaml file, you added the Git URL instead of a package dependency:

menubar:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/menubar

When you want to add a menu item in your app, you need to call the setApplicationMenu method, which is asynchronous and takes a List of Submenu objects.

Submenu in turn is a class that requires a label (the text you see on the menu) and a list of children, which can be MenuItem or MenuDivider objects. When you insert a MenuDivider in a SubMenu, a horizontal line separator will be added to the list of items in the menu.

MenuItem is what your users will actually click to perform the action you have provided in your app. In this recipe, you set its label, the enabled Boolean, and the onClicked callback that calls the updateBooks method:

MenuItem( 
label: 'Flutter',
enabled: true,
onClicked: () => updateBooks('flutter')),

The great thing is that depending on the system you are using, the menus will change accordingly. Here, you can see an example of the menu as it appears on a Windows desktop:

See also…

At the time of writing, the menu bar plugin is still a prototype. In the future, it might be published as an official package in pub.dev, or embedded in Flutter itself. To stay updated on this project, have a look at https://github.com/google/flutter-desktop-embedding/tree/master/plugins/menubar.

You can also add menus to Linux systems, and the process is quite similar to what was described in this recipe: see https://pub.dev/packages/flutter_menu for more details. 

Written by

XR Developer responsible for end-to-end development of XR solutions spanning multiple domains, by using various XR and WebXR libraries.

Leave a Reply