Enhancing Your Flutter Project with Typesafe Packages
Tags: flutter, dart, typesafe, tutorial
Published: 2024-06-03 12:37:27

# Introduction
Flutter's popularity has surged thanks to its ability to build beautiful and performant cross-platform applications. Large projects with many teams and developers often face a lot errors on a smaller or a larger scale due to the size of the project.

One of those problems is type safety.

_What is typesafe code?_

Remember all those times you added a new asset and used it in a widget. You run the app, but then it crashes because Flutter couldn’t find the asset? You probably forgot to add it to the `pubspec.yaml`.

I’ll give you another example.

You want to call methods from native code. Flutter provides Method Channels for this. You have to make sure that you call the correct method, with the correct parameters, otherwise, you’ll get an error like this:

```bash
Unhandled Exception: MissingPluginException(No implementation found for method getBatteryLevel on channel samples.flutter.dev/battery)
```

This is what typesafe code prevents from happening. Typesafe means **_that variables are statically checked for appropriate assignment at compile time._**

As projects grow, ensuring type safety becomes crucial to maintaining code quality and preventing runtime errors.

This blog will explore how to make your Flutter project more typesafe using various packages:

- Typesafe Method Channels: [pigeon](https://pub.dev/packages/pigeon)
- Typesafe REST APIs: [chopper](https://pub.dev/packages/chopper) and [retrofit](https://pub.dev/packages/retrofit)
- Typesafe GraphQL: [ferry](https://pub.dev/packages/ferry)
- Typesafe Routing: [go_router_builder](https://pub.dev/packages/go_router_builder)
- Typesafe Assets: [flutter_gen](https://pub.dev/packages/flutter_gen)
- Typesafe Environment Variables: [envied](https://pub.dev/packages/envied)

Let's dive into each of these and see how they can take your Flutter project from error-prone to error-free.

## Typesafe Method Channels with Pigeon

Method channels in Flutter facilitate communication between the Dart and native sides of an app (iOS and Android). Traditionally, method channels use dynamic typing, which can lead to runtime errors if the method names or argument types do not match.

`pigeon` eliminates these issues by generating type-safe code for method channels. It can also generate code for accessing a Dart method from native code, which can come in handy.

### Non-Typesafe Code

Typically you would write something like this in your Flutter project:

```dart
import 'package:flutter/services.dart';

class FlutterBridge {
  static const MethodChannel _channel = MethodChannel('com.example.app/channel');

  Future<String?> getMessage() async {
    final String? message = await _channel.invokeMethod('getMessage');
    return message;
  }

  Future<void> sendMessage(String message) async {
    await _channel.invokeMethod('sendMessage', {'text': message});
  }
}
```

And then write appropriate platform implementations:

- For Android (`MainActivity.kt` ):

```kotlin
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/channel"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            when (call.method) {
                "getMessage" -> {
                    val message = getMessage()
                    result.success(message)
                }
                "sendMessage" -> {
                    val text = call.argument<String>("text")
                    sendMessage(text)
                    result.success(null)
                }
                else -> {
                    result.notImplemented()
                }
            }
        }
    }

    private fun getMessage(): String {
        return "Hello from Android!"
    }

    private fun sendMessage(message: String?) {
        // Handle the message
    }
}
```

- For iOS (`AppDelegate.swift`):

```swift
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(name: "com.example.app/channel",
                                      binaryMessenger: controller.binaryMessenger)

    channel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "getMessage" {
        result(self.getMessage())
      } else if call.method == "sendMessage" {
        if let args = call.arguments as? [String: Any],
           let text = args["text"] as? String {
          self.sendMessage(message: text)
        }
        result(nil)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func getMessage() -> String {
    return "Hello from iOS!"
  }

  private func sendMessage(message: String) {
    // Handle the message
  }
}
```

### Typesafe Code with Pigeon

First, add `pigeon` to your `dev_dependencies` in `pubspec.yaml`:

```yaml
dev_dependencies:
  pigeon: ^19.0.1
```

Create a Dart file (e.g., `pigeons.dart`) to define the interface:

```dart
dartCopy code
import 'package:pigeon/pigeon.dart';

class Message {
  String? text;
}

@HostApi()
abstract class Api {
  Message getMessage();
  void sendMessage(Message message);
}
```

Run the Pigeon tool to generate the necessary code:

```bash
flutter pub run pigeon --input pigeons.dart
```

This generates platform-specific code ensuring that method calls are type-safe, reducing the risk of mismatches and errors.

## Typesafe GraphQL with Ferry

If you are using GraphQL, `ferry` provides a fully type-safe client by leveraging code generation. This ensures that queries, mutations, and subscriptions conform to the GraphQL schema, catching errors at compile time rather than at runtime. It also comes with a side package [`ferry_hive_store`](https://pub.dev/packages/ferry_hive_store) if you want to have offline persistence using Hive.

### Non-Typesafe Code

```dart
import 'package:graphql_flutter/graphql_flutter.dart';

class GraphQLService {
  final GraphQLClient _client;

  GraphQLService(this._client);

  Future<void> fetchData() async {
    const String query = '''
      query GetItems {
        items {
          id
          name
        }
      }
    ''';

    final QueryResult result = await _client.query(QueryOptions(document: gql(query)));
    if (result.hasException) {
      print(result.exception.toString());
      return;
    }

    final items = result.data?['items'];
    print(items);
  }
}
```

### Typesafe Code with Ferry

Add `ferry` and its code generation dependencies:

```yaml
dependencies:
  ferry: ^0.16.0+1
  gql_http_link: ^1.0.1+1

dev_dependencies:
  ferry_generator: ^0.10.0
  build_runner: ^2.4.10
```

Move your GraphQL query into a `.graphql` file:

```graphql
query GetItems {
        items {
          id
          name
        }
      }
```

Generate the necessary types by running:

```bash
flutter pub run build_runner build
```

Use your queries in a typesafe manner:

```dart
import 'package:ferry/ferry.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:ferry_http/ferry_http.dart';

final query = GFetchUserReq((b) => b..vars.id = 1);

final client = FerryClient(
  link: HttpLink('https://api.example.com/graphql'),
  cache: HiveStore(),
);

final response = await client.request(query).first;

if (response.hasErrors) {
  // Handle errors
} else {
  final user = response.data!.user;
  print('User: ${user.name}, Email: ${user.email}');
}
```

### Type-Safe REST API with Retrofit and Chopper

Both Retrofit and Chopper and type safe REST API generators, but they cover different libraries. Chopper is based on `http` package, while Retrofit is based on `dio` package. Both are inspired by Retrofit for Android.

### Non-Type-Safe Code

Directly using the `http` and `dio` requires manual handling of JSON parsing and lacks compile-time verification of API definitions, including path and query parameters.

### 1. HTTP

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<MyModel> fetchPost(int id) async {
  final response = await http.get(Uri.parse('https://api.example.com/posts/$id'));
  if (response.statusCode == 200) {
    return MyModel.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('Failed to load post');
  }
}
```

### 2. dio

```dart
dartCopy code
import 'package:dio/dio.dart';

Future<MyModel> fetchPost(int id) async {
  try {
    final response = await Dio().get('https://api.example.com/posts/$id');
    return MyModel.fromJson(response.data);
  } catch (e) {
    throw Exception('Failed to load post');
  }
}
```

### Typesafe Code with Chopper and Retrofit

Both Chopper and Retrofit use `build_runner` to generate the underlying network logic by wrapping around http and dio. By using annotations you can focus on defining API contracts and business logic without sacrificing type safety.

1. Chopper

```dart
import 'package:chopper/chopper.dart';

part 'api_service.chopper.dart';

@ChopperApi(baseUrl: '/posts')
abstract class ApiService extends ChopperService {
  @Get(path: '/{id}')
  Future<Response<MyModel>> getPostById(@Path() int id);
}
```

### 2. Retrofit

```dart
import 'package:retrofit/retrofit.dart';

part 'api_service.g.dart';

@RestApi(baseUrl: "https://api.example.com")
abstract class ApiService {
  factory ApiService(Dio dio, {String baseUrl}) = _ApiService;

  @GET("/posts/{id}")
  Future<MyModel> getPostById(@Path("id") int id);
}
```

Now you can call your APIs like any other method in the app.

## Typesafe Routing with GoRouterBuilder

Routing is a critical aspect of any Flutter app. Most used navigator packages are `go_router`. `auto_route`. AutoRoute provides typesafe and generated routes out of the box, while GoRouter relies on `go_router_builder` package.

### Non-Typesafe Code

```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) => HomeScreen(),
    ),
    GoRoute(
      path: '/details',
      builder: (BuildContext context, GoRouterState state) {
        final String id = state.queryParams['id'] ?? '';
        return DetailsScreen(id: id);
      },
    ),
  ],
);
```

### Typesafe code with GoRouterBuilder

`go_router_builder` integrates with the `go_router` package to provide type-safe routes.

Add dependencies:

```yaml
dependencies:
  go_router: ^14.1.4

dev_dependencies:
  go_router_builder: ^2.7.0
```

Define your routes in a Dart file:

```dart
import 'package:go_router/go_router.dart';

part 'app_routes.g.dart';

@TypedGoRoute<HomeRoute>(path: '/')
class HomeRoute {}

@TypedGoRoute<DetailsRoute>(path: '/details')
class DetailsRoute {
  final String id;

  DetailsRoute({required this.id});
}
```

Run the generator:

```bash
flutter pub run build_runner build
```

This will generate all the routes for you. The code generator combines all top-level routes into a single list called `$appRoutes` which you need to use to initialize the GoRouter instance:

```dart
final _router = GoRouter(routes: $appRoutes);
```
You can now navigate and pass parameters without worrying about something going wrong:

```dart
void _tap() => DetailsRoute(id: 'p1').go(context);
```

## Typesafe Assets with FlutterGen

Managing assets in Flutter can become cumbersome as your project scales. `flutter_gen` helps manage this by generating type-safe code for your assets. The package also provides utils for Lottie, Rive, and SVG files.

Always configure your assets in `pubspec.yaml`:

```yaml
flutter:
  assets:
    - assets/images/
```

### Non-Typesafe Code

```dart
Image.asset('assets/images/logo.png')
```

### Typesafe Code with FlutterGen

Add `flutter_gen`:

```yaml
dev_dependencies:
  flutter_gen: ^5.5.0+1
```

Run the generator:

```bash
flutter pub run build_runner build
```

Access your assets safely:

```dart
import 'package:my_app/gen/assets.gen.dart';

Image.asset(Assets.images.logo.path);
```

## Typesafe Environment Variables with Envied

Managing environment variables securely and type-safely can be challenging. `envied` makes it easy. Envied can also obfuscate your API keys to make them more secure, but beware, it’s just a `xor` of two strings and anybody who wants to steal your API keys will steal them.

Adding `envied`:

```yaml
dependencies:
  envied: ^0.5.4+1

dev_dependencies:
  build_runner: ^2.4.10
```

Create an `.env` file and a Dart class for the environment variables:

```
API_KEY=your_api_key
```

```dart
import 'package:envied/envied.dart';

part 'env.g.dart';

@Envied(path: '.env')
abstract class Env {
  @EnviedField(varName: 'API_KEY')
  static const String apiKey = _Env.apiKey;
}
```

Run the generator:

```bash
flutter pub run build_runner build
```

Access your environment variables safely:

```dart
final apiKey = Env.apiKey;
```

## Conclusion

By integrating these packages into your Flutter project, you can significantly enhance type safety, making your code more robust and maintainable. Leveraging `pigeon` for method channels, `ferry` for GraphQL,`chopper`and `retrofit` for REST APIs, `go_router_builder` for routing, `flutter_gen` for assets, and `envied` for environment variables, you can catch errors at compile time, ensuring a more reliable and error-free codebase.

If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on [**Twitter**](https://twitter.com/dinkomarinac) or [**LinkedIn**](https://www.linkedin.com/in/dinko-marinac/).

Until next time, happy coding!

Reposted from [my blog](https://dinkomarinac.dev/enhancing-your-flutter-project-with-typesafe-packages).