Adding Flutter to an Existing iOS Application: Part 2

Sandip SabaleSandip Sabale
5 min read

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.

  1. To use json_serializable package, add these dependencies to your pubspec.yaml.

    dependencies:

    json_annotation: ^4.9.0

    dev_dependencies:

    build_runner: ^2.3.3

    json_serializable: ^6.8.0

  2. Run flutter pub get to install the necessary packages

  3. Below 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 and DogBreed.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.id,

    this.name,

    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);

    }

  4. Run the dart run build_runner build command to generate the necessary code for JSON serialization. After successful execution, you will see the g.dart files created for SizeMetric.dart and DogBreed.dart.

  5. Add following import to in main.dart file,

    import 'package:my_flutter_module/DogBreed.dart';

  6. Replace MyHomePage class in main.dart file with following

    class 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),

    ),

    );

    }

    }

  7. Replace following in MyApp class in main.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;

    }

    },

  8. 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)

    }

  9. 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.

  1. To use the pigeon package, add these dependencies to your pubspec.yaml.

    dependencies:

    pigeon: ^20.0.1

  2. Create new schema.dart file under new folder pigeons 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({requiredthis.data});

    }

    @HostApi()

    abstract class ServiceApi {

    @async

    DogBreedResponse fetchDogBreed(String breedId);

    }

  3. 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

  1. Copy the generated serviceApi.swift into DogBreed application

  2. Implement ServiceApi protocol from generated serviceApi.swift in DogBreedService.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()

    }

    }

  3. Modify flutter_showDetailOfDogBreed(_ breed: DogBreed) function from DogBreedsViewController.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)

    }

  4. Add following import to main.dart file

    import 'package:my_flutter_module/service_api.dart';

    import 'dart:convert';

  5. Add following lines to _MyHomePageState class

    ServiceApi _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.

0
Subscribe to my newsletter

Read articles from Sandip Sabale directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sandip Sabale
Sandip Sabale