Adding Flutter to an Existing iOS Application: Part 2
In part 1, we have created a flutter module and integrated into an existing iOS i.e. DogBreed application. In this part, we will build DogBreed detail screen in flutter and demonstrate the use of Flutter's MethodChannel using Pigeon to make service calls from the host app
You can find the starter and completed projects on GitHub:
Step 1: Customize Flutter Module UI
We will use the json_serializable package to replicate the DogBreed.swift entity and its json parsing in flutter. The json_serializable package is a powerful tool that helps automate the process of converting between Dart objects and JSON data. It leverages code generation to eliminate the need for manual serialization code, reducing boilerplate and the risk of errors.
To use
json_serializable
package, add these dependencies to yourpubspec.yaml
.dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.3.3
json_serializable: ^6.8.0
Run
flutter pub get
to install the necessary packagesBelow is a template to create the dart class that you want to convert to and from JSON. It is required to keep the filename and dart class name the same
<template>
import 'package:json_annotation/json_annotation.dart';
part '<class name>.g.dart';
@JsonSerializable()
class <class name> {
// create constructor
factory <class name>.fromJson(Map<String, dynamic> json) => _$<class name>FromJson(json);
Map<String, dynamic> toJson() => _$<class name>ToJson(this);
}
With that, Create
SizeMetric.dart
andDogBreed.dart
file as following,-- SizeMetric.dart --
import 'package:json_annotation/json_annotation.dart';
part 'SizeMetric.g.dart';
@JsonSerializable()
class SizeMetric {
String imperial;
String metric;
SizeMetric(this.imperial, this.metric);
factory SizeMetric.fromJson(Map<String, dynamic> json) =>
_$SizeMetricFromJson(json);
Map<String, dynamic> toJson() => _$SizeMetricToJson(this);
}
-- DogBreed.dart --
import 'package:json_annotation/json_annotation.dart';
import 'package:my_flutter_module/SizeMetric.dart';
part 'DogBreed.g.dart';
@JsonSerializable()
class DogBreed {
int id;
String name;
SizeMetric weight;
SizeMetric height;
String? description;
String? bred_for;
String? breed_group;
String life_span;
String? temperament;
String? reference_image_id;
DogBreed(
this.weight,
this.height,
this.description,
this.bred_for,
this.breed_group,
this.life
_span,
this.temperament,
this.reference_image_id);
factory DogBreed.fromJson(Map<String, dynamic> json) =>
_$DogBreedFromJson(json);
Map<String, dynamic> toJson() => _$DogBreedToJson(this);
}
Run the
dart run build_runner build
command to generate the necessary code for JSON serialization. After successful execution, you will see theg.dart
files created forSizeMetric.dart
andDogBreed.dart
.Add following import to in
main.dart
file,import 'package:my_flutter_module/DogBreed.dart';
Replace
MyHomePage
class inmain.dart
file with followingclass MyHomePage extends StatefulWidget {
final String breedId;
const MyHomePage({super.key, required this.breedId});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
DogBreed? _breed;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
createDetailRow(rowData: 'Name: ${breed?.name ?? ""}'),
_createDetailRow(
rowData:
'Weight: ${_breed?.weight.metric ?? ""} kg (${_breed?.weight.imperial ?? ""} lbs)'),
_createDetailRow(
rowData:
'Height: ${_breed?.height.metric ?? ""} cm (${_breed?.height.imperial ?? ""} in)'),
_createDetailRow(
rowData: 'Description: ${_breed?.description ?? ""}'),
createDetailRow(rowData: 'Bred For: ${breed?.bred_for ?? ""}'),
_createDetailRow(
rowData: 'Breed Group: ${_breed?.breed_group ?? ""}'),
createDetailRow(rowData: 'Life Span: ${breed?.life_span ?? ""}'),
_createDetailRow(
rowData: 'Temperament: ${_breed?.temperament ?? ""}'),
],
),
),
);
}
Widget _createDetailRow({required String rowData}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Text(
rowData,
style: const TextStyle(fontSize: 16),
),
);
}
}
Replace following in
MyApp
class inmain.dart
file,home: const MyHomePage(title: 'Flutter Demo Home Page'),
with
onGenerateRoute: (settings) {
var uri = Uri.parse(
settings.name
!);
String? route = uri.path;
Map? queryParameters = uri.queryParameters;
switch (route) {
case "/DogBreedDetailScreen":
return MaterialPageRoute(
builder: (context) => MyHomePage(
breedId: queryParameters["id"] ?? "",
),
);
default:
return null;
}
},
Add following function to
DogBreedsViewController.swift
func flutter_showDetailOfDogBreed(_ breed: DogBreed) {
let flutterViewController = FlutterViewController(project: nil,
initialRoute: "/DogBreedDetailScreen?id=\(
breed.id
)",
nibName: nil,
bundle: nil)
self.navigationController?.show(flutterViewController,
sender: nil)
}
Call above function from table view delegate as following
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
self.flutter_showDetailOfDogBreed(self.dogBreeds[indexPath.row])
}
Now, Run the application. you will see the flutter Dog breed detail screen.
Step 2: Create a Pigeon
What is MethodChannel?
Method Channel is a Flutter API that allows communication between Flutter and the host platform (iOS or Android). It uses method calls to send messages from Flutter to the host platform and receive responses. But it is prone to errors due to string-based method names and arguments.
What is Pigeon?
Pigeon is a code generator tool to make communication between Flutter and the host platform type-safe, easier, and faster. Pigeon removes the necessity to manage strings across multiple platforms and languages and write custom platform channel code.
To use the pigeon package, add these dependencies to your
pubspec.yaml
.dependencies:
pigeon: ^20.0.1
Create new
schema.dart
file under new folderpigeons
as following,import 'package:pigeon/pigeon.dart';
// configuration: These options can also be specified on generate command
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/service_api.dart',
dartOptions: DartOptions(),
swiftOut: 'lib/pigeons/serviceApi.swift',
swiftOptions: SwiftOptions(),
))
// configuration
class DogBreedResponse {
final String data;
DogBreedResponse({required
this.data
});
}
@HostApi()
abstract class ServiceApi {
@async
DogBreedResponse fetchDogBreed(String breedId);
}
Navigate to
my_flutter_module
directory and run,dart run pigeon --input lib/pigeons/schema.dart
You will see the following generated files,
serviceApi.swift file under pigeons directory
service_api.dart file under lib directory
Step 3: Link the Pigeon with the Host App
Copy the generated
serviceApi.swift
into DogBreed applicationImplement
ServiceApi
protocol from generatedserviceApi.swift
inDogBreedService.swift
as following,extension DogBreedService: ServiceApi {
func fetchDogBreed(breedId: String,
completion: @escaping (Result<DogBreedResponse, Error>) -> Void) {
guard var url = URL(string: apiURL) else { return }
url.append(path: breedId)
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
let breedData = String(data: data, encoding: .utf8)
let result = DogBreedResponse(data: breedData ?? "")
completion(.success(result))
}
}.resume()
}
}
Modify
flutter_showDetailOfDogBreed(_ breed: DogBreed)
function fromDogBreedsViewController.swift
func flutter_showDetailOfDogBreed(_ breed: DogBreed) {
let flutterViewController = FlutterViewController(project: nil,
initialRoute: "/DogBreedDetailScreen?id=\(
breed.id
)",
nibName: nil,
bundle: nil)
ServiceApiSetup.setUp(binaryMessenger: flutterViewController.binaryMessenger,
api: DogBreedService.shared)
self.navigationController?.show(flutterViewController,
sender: nil)
}
Add following import to main.dart file
import 'package:my_flutter_module/service_api.dart';
import 'dart:convert';
Add following lines to
_MyHomePageState
classServiceApi _service = ServiceApi();
@override
void initState() {
super.initState();
_loadDogBreedData();
}
_loadDogBreedData() async {
try {
var result = await _service.fetchDogBreed(widget.breedId);
Map<String, dynamic> breedData = jsonDecode(
result.data
);
setState(() {
_breed = DogBreed.fromJson(breedData);
});
} catch (e) {
print("Error from host app $e");
}
}
Now, run the application. You will see the Flutter Dog Breed detail screen with data being fetched from the host app.
That's it! We've successfully added a Flutter module to an existing iOS application and demonstrated the use of Flutter's MethodChannel using Pigeon for communication between the Flutter module and the iOS host application.
For the complete code and more details, refer to:
Part 1 - Focused on creating a Flutter module and integrating it into an iOS application.
Part 2 - Focused on communication between the Flutter module and the host application using Flutter's MethodChannel via Pigeon.
Subscribe to my newsletter
Read articles from Sandip Sabale directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by