Create a Linux package browser in 5 minutes with Flutter 3
David Serrano
Posted on June 1, 2022
In this latest article about what's new in Flutter 3, I'm going to talk about the stable support for Linux applications. In previous articles I have already talked about the performance improvements in Flutter web and for macOS applications, as well as the new features that Dart 2.17 brings. Stable support for Linux comes full circle to make Flutter one of the best cross-platform technologies out there.
In terms of performance, the tests that I have done do not show a significant improvement, so I understand that what they have done is simply make Linux official as a stable platform because it is in a sufficiently mature state, and the proof of that is that Canonical has used it as a tool to build some of its applications for Ubuntu.
Speaking of Canonical, they are the ones who have contributed much of the Linux-oriented plugins that we can find in pub.dev. In this article I am going to create a small application that will allow us to search for snap packages, using the yaru visual style, which is Ubuntu's own style.
For those who don't know what snap is: snap is a package manager for Linux. It is created and maintained by Canonical, and although it is not the most popular package manager, it will serve us perfectly in this tutorial to see the interconnection that we can do from Flutter with the operating system.
Create Linux application
Let's start by creating a new project and adding all the necessary dependencies:
flutter create flutter_3_linux
cd flutter_3_linux
flutter pub add yaru yaru_icons snapd provider url_launcher
The dependencies we have added are:
- yaru: it will help us to stylize the application with the visual style of Ubuntu
- yaru_icons: provides yaru style icons
- snapd: this is the plugin that will allow us to interact with the snap client, it is important to mention that this client has to be previously installed in the system
- provider: I will use this package to manage the state of the application
- url_launcher: it will help us to open the page of the official store for the snaps that we look for within the application
Now I'm going to create a model class to handle state, a main screen, and I'm going to modify the main.dart
file as follows:
// lib/main_model.dart
import 'package:flutter/material.dart';
import 'package:snapd/snapd.dart';
class MainModel extends ChangeNotifier {
final SnapdClient _client = SnapdClient();
@override
void dispose() {
super.dispose();
_client.close();
}
}
// lib/main_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_3_linux_2/main_model.dart';
import 'package:provider/provider.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainModel>(
create: (_) => MainModel(),
child: Consumer<MainModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: const Text('Flutter 3 Linux'),
),
),
),
);
}
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_3_linux_2/main_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MainScreen(),
);
}
}
If you run the app now you'll see that it has a typical Material look and feel:
The way to solve it is to modify main.dart
as follows:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_3_linux_2/main_screen.dart';
import 'package:yaru/yaru.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// Wrap your first screen with YaruTheme to apply Yaru style
// to all widgets underneath it
home: const YaruTheme(
child: MainScreen(),
),
);
}
}
As you can see, all we have to do is wrap the screen with YaruTheme
, this will automatically apply the Yaru style to all the widgets below it. This way we won't have to worry about using any more yaru-style widgets, we'll just add regular widgets from the material package.
Add search bar
Now we are going to make it so that the user can search for snap packages by typing in a search criteria. Add the required variables to main_model.dart
to manage the state of the search in the UI:
// lib/main_model.dart
String _searchQuery = '';
String get searchQuery => _searchQuery;
set searchQuery(String value) {
if (value != _searchQuery) {
_searchQuery = value;
notifyListeners();
}
}
Modify main_screen.dart
to add the search bar:
// lib/main_screen.dart
class _MainScreenState extends State<MainScreen> {
// Add this variables to control the search field
final _searchController = TextEditingController();
final _searchFocus = FocusNode();
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainModel>(
create: (_) => MainModel(),
child: Consumer<MainModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: const Text('Flutter 3 Linux'),
),
body: Column(
children: [
Container(
padding:
const EdgeInsets.only(left: 16.0, top: 16.0, right: 16.0),
child: TextFormField(
controller: _searchController,
focusNode: _searchFocus,
keyboardType: TextInputType.text,
decoration: InputDecoration(
icon: const Icon(YaruIcons.search),
suffixIcon: model.searchQuery.isEmpty
? null
: IconButton(
icon: const Icon(
YaruIcons.edit_clear,
size: 24.0,
),
onPressed: () {
model.searchQuery = '';
_searchController.clear();
},
),
hintText: 'Search a snap...',
),
onChanged: (String term) => model.searchQuery = term,
),
),
],
)),
),
);
}
}
This way we get a search bar in which we can write the snap package we want:
Optimizing the search mechanism
Before we get into the actual package search, let's declare how we want it to work:
- We want snap packages to be searched for when the user types characters in the search bar
- The search is dynamic, so as the user types characters it has to change
- It has to be efficient
The problem with this scheme is that as soon as the user types a character, a search request will be launched, which in turn will lead to a network request. In other words, if I write 10 characters in a row, the snap client will make 10 requests in a row. This is far from efficient. Let's modify the search as follows, and then see the effect we've achieved:
// lib/main_model.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:snapd/snapd.dart';
class MainModel extends ChangeNotifier {
// We'll wait for the user to stop writing for this amount of delay
static const _searchDelay = 1;
final SnapdClient _client = SnapdClient();
String _searchQuery = '';
String get searchQuery => _searchQuery;
set searchQuery(String value) {
if (value != _searchQuery) {
_searchQuery = value;
_search(); // <-- Add this call here
notifyListeners();
}
}
Timer? _timer;
@override
void dispose() {
super.dispose();
_client.close();
}
void _search() {
_timer?.cancel();
if (_searchQuery.isNotEmpty) {
_timer = Timer(const Duration(seconds: _searchDelay), () async {
// Perform the snap search here
});
}
}
}
Now the search will be executed only when the user finishes typing, that is, when 1 second has passed since the last time a character was entered. Even so, the search is still dynamic, if they enter more characters, the search request will be launched again.
Searching for packages and displaying them
To finish, we are going to search for snap packages using the snapd
plugin. To display them in the UI I'm going to use a Stream:
// lib/main_model.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:snapd/snapd.dart';
class MainModel extends ChangeNotifier {
static const _searchDelay = 1;
final SnapdClient _client = SnapdClient();
String _searchQuery = '';
String get searchQuery => _searchQuery;
set searchQuery(String value) {
if (value != _searchQuery) {
_searchQuery = value;
_search();
notifyListeners();
}
}
Timer? _timer;
// Add this controller and its stream here.
final StreamController<List<Snap>> _snapController = StreamController();
late final Stream<List<Snap>> snapStream;
@override
void dispose() {
super.dispose();
_client.close();
_snapController.close(); // <-- Don't forget to close the controller.
}
void _search() {
_timer?.cancel();
if (_searchQuery.isNotEmpty) {
_timer = Timer(const Duration(seconds: _searchDelay), () async {
// Search for packages and stream them
final snaps = await _client.find(query: _searchQuery);
_snapController.sink.add(snaps);
});
} else {
// If the search is empty, stream an empty array
_snapController.sink.add([]);
}
}
}
Add the following widget as the second element of the Column in main_screen.dart
, just below the Container that wraps the search bar:
Expanded(
child: Container(
padding: const EdgeInsets.only(
left: 16.0, top: 16.0, right: 16.0, bottom: 16.0),
child: StreamBuilder<List<Snap>>(
stream: model.snapStream,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
final snaps = snapshot.data!;
return ListView.separated(
itemBuilder: (context, index) {
final snap = snaps[index];
return ListTile(
title: Text(snap.title),
subtitle: Text(
snap.summary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
snap.version,
style: Theme.of(context).textTheme.caption,
),
onTap: snap.storeUrl != null
? () async {
final uri = Uri.parse(snap.storeUrl!);
if (await canLaunchUrl(uri)) {
launchUrl(uri);
}
}
: null,
);
},
separatorBuilder: (context, index) => Container(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
),
child: const Divider(),
),
itemCount: snaps.length);
} else {
return Container();
}
},
),
),
)
You can now search for snaps packages by typing characters in the search bar. You can also click on them to open their file on the web:
Conclusion
In this article I have taken a simple walkthrough of building a Linux application using Flutter. If you haven't seen it yet, I invite you to check out the other articles I've written about the improvements that have been applied to Flutter 3.
You can find the complete source code of this tutorial here.
Thank you for reading this far.
Happy coding!
Posted on June 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.