Flutter’s Best Practices for High-Performing and User-Friendly Apps for 2023 (II)

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


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:

Flutter’s Best Practices for High-Performing and User-Friendly Apps for 2023 (II) - APK

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

Leave a Comment