There were enough excellent methods and approaches that we couldn’t help but create a second version of Flutter’s Best Practices for High-Performing and User-Friendly Apps for 2023 (II). Let’s continue…
This blog post shall cover the following efficient and reliable methods that are listed below.
Table of Contents
- 1- Using Navigations professionally
- 2- Use Custom Linter rules
- 3- Build split APK bundles
- 4- Commenting on the code and preparing documentation side-by-side
- 5- Using imports & exports
- 6- Flutter Secure Storage for local data
- 7- String Interpolation
- Wrapping Up
1- Using Navigations professionally
Flutter has a variety of routes available that include:
Anonymous
Named Routes
onGenerateRoutes (Using them explicitly)
Let us dive into all of these and understand them as chunks.
– Anonymous
Consider we have two screens, namely screen_a.dart
and screeb_b.dart
respectively, and use the route in the following form:
Navigator.push(context, MaterialPageRoute(builder: (_) => ScreenTwo()));
This approach may be considered good if you’re just trying out Flutter, or you’re in your initial phase of learning. Secondly, if you’ve gained mid-level experience, you should use NamedRoutes instead of this.
Note: This form of route sometimes becomes handy to use the same logic again and again and one might lose interest quickly.
– Named Routes
For this, we don’t need to build the entire logic as we did in the first one. Instead, we should only use a string route with a context such as:
Navigator.pushNamed(context, '/route_two');
We’re halfway done. Now we need to point towards MaterialApp and do some configurations.
Instead of defining a route for the home entity, we need to set:
initialRoute: '/'
Followed by defining routes in the “routes entity” having the type Map such that:
routes: {
'/': (context) => FlutterBestPractices(),
'/route_two': (context) => RouteTwo(),
},
At this point, restart the app, and we’re good to go.
– onGenerateRoutes (Using them explicitly)
This is a bit tricky but, it is the best and most professional form of Navigations in Flutter.
Defining our routes in a separate class and importing them into MaterialApp with some mandatory configurations. We call it onGenerateRoutes.
Under the project, create a new dart file as ‘app_router.dart‘ and create a new method as onGenerateRoutes with one parameter like RouteSettings (to retrieve the route name).
app_router.dart
import 'package:flutter/material.dart';
import 'package:flutter_useful_tasks/tasks/dynamic_formfields.dart';
import 'package:flutter_useful_tasks/tasks/flutter_best_practices.dart';
import 'package:flutter_useful_tasks/widgets/custom_text.dart';
class AppRouter {
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
switch(settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const FlutterBestPractices());
case '/dynamic':
return MaterialPageRoute(builder: (_) => const DynamicFormFields());
default:
return MaterialPageRoute(builder: (_) => Scaffold(
body: Center(
child: CustomText(text: 'No route found'),
),
));
}
}
}
The above method uses a switch case to identify the valid route via its title, followed by a “No route found” screen in case of an incorrect one.
Additionally, we need to initialize this class inside our MaterialApp component.
Note: Convert your MyApp class to Stateful Widget to use initState for and define the object.
AppRouter? router;
@override
void initState() {
// TODO: implement initState
router = AppRouter();
super.initState();
}
Don’t forget to define the initial route and onGenerateRoute entity under MaterialApp.
@override
Widget build(BuildContext context) {
// TODO: implement build
return const MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/',
onGenerateRoute: AppRouter.onGenerateRoute,
);
}
We’re done.
The rest of the approach would remain the same. Such that:
Navigator.pushNamed(context, '/route_two');
Restart the app and see the result.
By using this approach, not only did we separate the complete list of our routes, but made our code clean and reduced the memory load too.
2- Use Custom Linter rules
Linitng in Flutter is a pretty awesome technique to improve the performance of the app and code clarity. Before Flutter version 2.30, linting was implemented manually using the package flutter_lints or via the command flutter create ., but now it is included when creating a new project.
You may have seen these warnings during the development:
- use_key_in_widget_constructors
- avoid_print
These are pre-associated as linter rules.
What if we need to define our own rules? Well, we can do that by heading over to analysis_options.yaml
where we should place the rules.
In this file, you will find the initial code snippet:
include: package:flutter_lints/flutter.yaml linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
Note: We need to make sure to provide the correct indentation otherwise it will fail.
Considering the warning we discussed above, we shall disable the first one, so how do we do that?
rules:
use_key_in_widget_constructors: false
Moreover, there are a lot of them that you can work on. Find them here.
Wait for a bit, and now you won’t see the suggestion to add the key to construction. You may adjust according to your needs.
3- Build split APK bundles
APK (Android Package Kit) is a compilation of binary architectures, associated with project resources as well.
flutter build apk
It is the quintessential command we use once we’re done with our project. There is nothing wrong with it, but it generates a huge file-size (FAT) APK that would not be suitable for users on the Play Store, especially if the app starts to go big in the market.
As a solution to that, Flutter introduced:
flutter build apk --split-per-abi
Abi – Application Binary Interface
This command is quite useful as it generates separate binary architectures for different platforms, and reduces APK file size to a certain extent.
By running this command we will get an output as follows:
In this picture, v7a-release is the supported architecture for Android.
4- Commenting on the code and preparing documentation side-by-side
Always make sure to add:
/// task_short_summary (highlighted in green) TODO: task_description (highlighted in yellow)
At first, it might sound boring, but as your app grows, it will make more sense and will ensure excellent documentation side-by-side.
Additionally, modify the GitHub’s readme file as you progress alongside the app. Including screenshots, code snippets, etc.
5- Using imports & exports
As our project gets bigger, we separate the .dart files into chunks for best practice, so for them, you might have seen a long list of imports, making the code complexity a bit difficult for other developers.
Here’s a sample
import 'package:flutter/material.dart';
import 'package:flutter_useful_tasks/tasks/dynamic_formfields.dart';
import 'package:flutter_useful_tasks/tasks/pickers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_useful_tasks/tasks/dynamic_formfields.dart';
import 'package:flutter_useful_tasks/tasks/pickers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_useful_tasks/tasks/dynamic_formfields.dart';
import 'package:flutter_useful_tasks/tasks/pickers.dart';
/* replace import with export in all above'
export 'package:flutter_useful_tasks/tasks/dynamic_formfields.dart';
export 'package:flutter_useful_tasks/tasks/pickers.dart';
Flutter supports export as a solution.
Create a new file export.dart,
copy all the imports, and place them inside this.
And then import this file as a replacement. That’s all!
Note: Create a relevant list of imports on the exports file for a better structure.
6- Flutter Secure Storage for local data
It’s another critical and vital part of an application to store the response locally. Most developers prefer using Shared Preferences (uses key-value pair) as it provides support for a variety of data types, namely
Code snippets for clarity
SharedPreferences sp = await SharedPreferences.getInstance();
sp.setBool('dummy_bool', false);
sp.setInt('dummy_int', 55);
List<String> doctors = ['Ahmed', 'Javed', 'Sheraz'];
sp.setStringList('doctor_list', doctors);
// Remove values
sp.remove('dummy_int');
sp.clear(); -> all clear
Best use-case
Used for auth_id, biometric code, tokens, etc.
However, there is one more package that is less talked about, but relatively better than preferences i.e. Flutter Secure Storage.
https://pub.dev/packages/flutter_secure_storage
Why is it better?
Well, it provides separate encryptions for Android and iOS and uses secure storage to do that.
Note: iOS uses Keychain Services to store certificates, AES for Android, further wrapped with RSA, and as of version 5 and above, we can now make use of:
encryptedSharedPreferences: true
Which claims an additional level of security, particularly for Android.
Code snippet for Object creation
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class LocalStorage {
LocalStorage._();
static AndroidOptions options = const AndroidOptions(encryptedSharedPreferences: true);
static FlutterSecureStorage secureStorage = FlutterSecureStorage(aOptions: options);
}
Additionally, we can also apply iOS options.
static IOSOptions iosOptions = const IOSOptions(
accessibility: KeychainAccessibility.passcode,
accountName: AppleOptions.defaultAccountName,
);
static FlutterSecureStorage secureStorage =
FlutterSecureStorage(aOptions: options, iOptions: iosOptions);
Code snippets
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class LocalStorage {
LocalStorage._();
static AndroidOptions options = const AndroidOptions(encryptedSharedPreferences: true);
static IOSOptions iosOptions = const IOSOptions(
accessibility: KeychainAccessibility.passcode,
accountName: AppleOptions.defaultAccountName,
);
static FlutterSecureStorage secureStorage =
FlutterSecureStorage(aOptions: options, iOptions: iosOptions);
dynamic writeData(String key, String value) async {
await secureStorage.write(key: key, value: value);
}
/// Storing encoded JSON
dynamic writeResponse() async {
String response = jsonEncode(httpResponse.body);
await secureStorage.write(key: 'response_key', value: response);
}
dynamic readData(String key) async {
return await secureStorage.read(key: key) ?? 'N/A';
}
dynamic deleteAll() async {
await secureStorage.deleteAll();
}
}
After analyzing, both plugins are best in their use-case, but flutter_secure_stoage wins the race, as it provides extended encryption not only for Android and iOS but, for other platforms too.
Now, let’s head over to the last one…
7- String Interpolation
As of the latest updates in Flutter, the old approach of using two or more strings was like this:
/// old
String dummy = "Dummy";
String concatenate = 'Dummy' + dummy;
debugPrint(concatenate);
It is considered a bad approach and Flutter’s dart analyzer would suggest you use string interpolation, which is something like this:
/// New
String data = "\nThis is the dummy data you've been looking for\n";
String con = 'Dummy Data $data';
/// in case of api models
String con = 'Dummy Data ${model.data}';
We’ve reached the end of this blog.
Thanks for your precious time.
Wrapping Up
Hope you enjoyed every bit of it.
So, as a continuation of Flutter’s Best Practices for High-Performing and User-Friendly Apps for 2023 blog post, we learned other tips and tricks, followed by some code snippets, usage of plugins for clarity, and other stuff.
If you think I missed out on something or want to add your point of view, please jot it down in the comments section below. I would love to know that as well.
Link to GitHub Repositories, and YouTube Channel
Also, read Flutter’s Stateful Widget LifeCycle
Read out previous blogs – Click here