Flutter Web: Problems, Solutions, and My Conclusion
I’ve been assigned a new project at work — to build our company website using Flutter. While doing so, I encountered some problems that I had not occurred while developing an app using Flutter, so I thought I’d share them. But before I do, here’s my conclusion as to building a web “app” using Flutter. Please take it with a grain of salt as I only built one web page with it and and may not be experienced enough.
If you’re building a very simple website, I think it should be fine. But anything more complex in structure, you should stick to JavaScript. Although Flutter Web does provide you with convenience, it had more hurdles than I expected. Here are the hurdles/other information:
(1) Url — Query Parameters (and Leading Hash)
(2) How to Run Flutter Web on Mobile Browser?
(3) How to Know What the “Platform” is?
(3) How to Change Title & Favicon Icon
Url — Query Parameters (and Leading Hash)
First of all, Flutter’s web apps start with hash (#) — I’m not sure why, but it seems unnecessary…but thankfully, it can be removed with the url_strategy package. Done!
Before going on to onGenerateRoute — page transitions. Without any configuration, Flutter will use the default transition for mobiles phones, which is usually not what we want for web. So, I use the below transition (which basically is “no transition” with no animation).
import 'package:flutter/material.dart';
class CustomPageRoute<T> extends MaterialPageRoute<T> {
CustomPageRoute({required Widget page, required String routeName})
: super(builder: (BuildContext context) => page, settings: RouteSettings(name: routeName));
@override
Widget buildTransitions(BuildContext context, _, __, Widget child) {
return child;
}
}
Now, what about the url? For web apps, we need to use onGenerateRoute.
onGenerateRoute: (RouteSettings route) {
/// for when page is pushed using Navigator
if (route.name == "/") {
return CustomPageRoute(page: HomePage(contactUsType: _contactUsType), routeName: HomePage.routeName);
}
if (route.name == StoresPage.routeName) {
final int _pageIndex = route.arguments as int;
return CustomPageRoute(page: StoresPage(isPushed: true), routeName: "/stores?page=${_pageIndex.toString()}");
}
if (route.name == StoreDetailPage.routeName) {
final String _storeId = route.arguments as String;
return CustomPageRoute(page: const StoreDetailPage(), routeName: "/stores?store_id=$_storeId");
}
/// for when page is refreshed and there is a query variable (if no variable, this part is not needed)
if (Uri.base.hasQuery) {
final String? _pageIndex = Uri.base.queryParameters["page"];
final String? _storeId = Uri.base.queryParameters["store_id"];
if (_pageIndex != null) return CustomPageRoute(page: StoresPage(), routeName: "/stores?page=$_pageIndex");
if (_storeId != null) return CustomPageRoute(page: StoreDetailPage(), routeName: "/stores?store_id=$_storeId");
}
return CustomPageRoute(page: const HomePage(), routeName: HomePage.routeName);
},
),
When you push using Navigator, you can put an argument in, which you can receive through route.arguments and use that to pass onto your page. You CANNOT use ModalRoute.of(context)?.settings.arguments in the page to receive the arguments in Flutter web. In the above code, I used it to set my query parameter so that I can display the url like a proper website. Notice that StoresPage and StoreDetailPage have different routeNames, but the actual url begins with the same path: /stores.
await Navigator.of(context).pushNamed(StoreDetailPage.routeName, arguments: _stores[index].storeId);
In the second part of onGenerate, I use Uri. to find information related to the url (like base, host, path, query, has query, query parameters etc). I added this portion because when I refresh the browser, all my state variables saved in my providers are lost, and any page with query variables will not be directed to the appropriate page (with just the top part of the above code). So I use the query value to direct it to the appropriate page, and if necessary, restore the appropriate state variables.
Oddly, when pushing the same page that had a query value that was different from the page pushing it (for example, from stores?page=1 to stores?page=2), the page loaded twice. When debugging it, I found out that it was loading again because the query variable changed (to the previous value). This was weird and I still do not have an answer to this.
Final word, when pushing the same page but with a different query value, I found that it was better to pushReplacementNamed than just push because that means the same page is pushed repetitively, resulting in multiple reloads.
How to Run Flutter Web on Mobile Browser?
We definitely want to test how the webpage works on the mobile browser…but I struggled initially because it wouldn’t load my lotteflies nor detect any gestures. But I finally found the solution:
flutter run -d web-server --web-port 1234 --web-hostname <your_ip_address> --web-renderer canvaskit
Honestly, I don’t know enough about server/internet connections, so I explain the above code with 100% certainty, but I will share what I deduced. So, for you to open your web app on a mobile browser, you need to open it on a specific port (web-server — web-port 1234) (here, the port is 1234).
The — web-hostname specifies what will be the host of that port. You can put your ip address there (type ifconfig in terminal, and in en0, you should see your ip address after “inet”) or you can just put 0.0.0.0 (which tells the server to listen on all available network interface — which I presume…that if you connected from a different ip address with port 1234…your app might pop up?). Either way, you should be able to open your web app on your mobile browser if you put the following in the url:
http://<ip_address>:1234
If you go to your ip site by putting in your ip address in the url, you could play with the the port forwarding configuration so that your coworkers can see your work (you’d have to run your application “server” first).
There are two different web renderers — HTML and CanvasKit. If you run your web app without specifying which renderer, it will automatically select HTML on a mobile browser and CanvasKit on a desktop browser. I found that the HTML renderer does not allow me to use gestures (scroll or tap a button) nor load my lottieflies animation. While the HTML renderer has a benefit of being much smaller in size, it is not fully consistent with Flutter mobile and desktop functions, because it uses only HTML elements, CSS, Canvas elements, and SVG elements.
However, when you use CanvasKit, there are other problems. For example, you cannot inspect your code through chrome, nor will some elements that appear fine when built with HTML not appear (ellipsis will appear with odd characters). The font size, font weight, and some other UI features seem to be different as well.
Also, I encountered XMLHttpRequest Error when trying to connect to our server (I wasn’t able to verify whether this is the same when the app is rendered using HTML), which my backend developer solved by changing some configurations on Nginx, and I was able to debug on chrome on my MacBook. However, the problem still persisted when debugging on the mobile browser.
My thoughts: I think it’s better to render with HTML if possible, and only switch to CanvasKit if necessary.
How to Know What the “Platform” is?
I was so used to using Platform.isAndroid, Platform.isIOS etc. to determine the OS of the user’s device, but the dart:io library from which it is imported does not work with Flutter web apps. Thankfully, stack overflow gave me a solution:
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
static const routeName = "/";
@override
Widget build(BuildContext context) {
return defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.android
? const HomeMobile()
: const HomeWeb();
}
}
I ended up adding a width boolean to the above code because I wanted the desktop version to be loaded to tablets and iPads.
How to Change Title & Favicon Icon
Icon: (1) add image to web/icons of your build folder and (2) in your html file in the web build folder, replace the favicon line with
<link rel="shortcut icon" type="image/png" href="icons/your-icon-name.jpg" />
Title: add title value to MaterialApp.
I’m not sure whether I’m the only one that struggled with Flutter Web, but for now, I do think convenience comes at a cost. Maybe if you use html within your code, it could work better, like how sometimes, you want to use native code in your Flutter application.
Happy coding!