Tomáš Repčík - 8. 3. 2026

Targets, Build Types, Flavors and Schemes

How Flutter works with different targets, build types, flavors and schemes

2026

Flutter tries to unite everything under one roof, so you do not need to think about different platforms, build types, flavors and schemes.

However, the way Android and iOS work under the hood is different, and it can get confusing why you need to do certain things on every platform.

This is not a full-blown guide to all the concepts, but my view on the concepts and how they work together. Mainly, what to look for when you solve certain categories of problems.

What are they?

For those, who do not know, here is a quick overview of the concepts:

Why am I writing about this?

I am working with apps, which have different flavors and schemes for many years now, but only now I have thought about writing about it.

From this point, if I write scheme or flavor, I will be talking about both platforms, because they are similar concepts, just with different names.

If you plan on delivering your app with different flavors, you have this handy tutorial from flutter how to handle it: Flutter Flavors. You can cover the Android and iOS and after tweaking the projects, you are done and from the most part you do not need to care.

The issue arises, when suddenly you need to add some specific native code.

In Android, you can do it quite easily with source sets. You will create new folders, write implementations for the specific flavor and build type, and you are done.

In iOS, I was always struggling. Feel free to call it a skill issue or lack of knowledge, but there was no clear way for me. I was encountering leftover code or some duplicate symbols as libraries were included in the wrong targets, and I was not sure how to solve it.

In the end, I have found out a couple of ways how to deal with it, but I have never found a clear guide on how to do it, and what are the best practices.

📧 Get more content like this in your inbox

You want to add some native code for specific flavor/scheme?

In Android, just look up how to use source sets and you are done.

In iOS, you have a couple of options:

func configureFeatures() {
    #if PRO_VERSION
    print("Enable 4K Video Export")
    #else
    print("Show banner ads")
    #endif
}
// EnvironmentConfig_Dev.swift -> Checked only for Dev Target.
// EnvironmentConfig_Prod.swift -> Checked only for Prod Target.
// In EnvironmentConfig_Dev.swift
struct Environment {
    static let apiBaseURL = "https://staging-api.myapp.com"
    static let logLevel: LogLevel = .verbose
}

// In EnvironmentConfig_Prod.swift
struct Environment {
    static let apiBaseURL = "https://api.myapp.com"
    static let logLevel: LogLevel = .error
}

// In your shared ViewController.swift
// Both targets can call this because 'Environment' exists in both.
let url = Environment.apiBaseURL
// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "CoreModule",
    products: [
        .library(name: "CoreModule", targets: ["CoreModule"]),
    ],
    dependencies: [
        ...
    ],
    targets: [
        .target(
            name: "CoreModule",
            dependencies: [ 
                // This package will only be included in iOS platform
                .product(name: "Package 1", package: "Package-1", condition: .when(platforms: [.iOS])),
                // This package will only be included in Debug configuration
                .product(name: "Package 2", package: "Package-2", condition: .when(configuration: .debug))
            ]
        ),
    ]
)
# Shared pods used by everyone
def shared_pods
  pod 'package-1'
  pod 'package-2'
end

target 'MyApp-Free' do
  shared_pods
  pod 'package-ads' # Only for Free
end

target 'MyApp-Pro' do
  shared_pods
end

The unfortunate part is, when you start digging into the packages, you can lose the default Flutter plugin registration flow and need to register plugins manually, which can be a bit of a pain, but it is doable. You can use the generated plugin registrant code in the iOS project as a reference and register the needed plugins manually in the AppDelegate.

When you want to remove package

This is actually the core reason for writing this small article.

I was creating one app, which had 3 packages using LLMs under the hood.

Cactus, MediaPipe and Executorch. (More on why, in the near future).

Unfortunately, two of them were giving duplicated symbols, but I really wanted to use both of them. It would require a lot of work to separate them, so I decided in the end to create two targets.

ExecuTorch and MediaPipe were in one target using similar dependencies and that is why they were giving duplicated symbols.

Since, we are in Flutter, I had to create manual registration of the plugins instead of automatic, which was tedious.

import Flutter
import UIKit

@main
@objc class AppDelegateExecuTorch: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    let registry = engineBridge.pluginRegistry
    registerPlugin(
      pluginKey: "FPPBatteryPlusPlugin",
      className: "FPPBatteryPlusPlugin",
      moduleName: "battery_plus",
      with: registry
    )
    ...
  }
}

private func registerPlugin(
  pluginKey: String,
  className: String,
  moduleName: String,
  with registry: FlutterPluginRegistry
) {
  let candidateNames = [
    className,
    "\(moduleName).\(className)",
  ]

  guard let pluginClass = candidateNames
    .compactMap({ NSClassFromString($0) as? FlutterPlugin.Type })
    .first else {
    assertionFailure("Missing Flutter plugin class \(className) (\(moduleName))")
    return
  }

  guard let registrar = registry.registrar(forPlugin: pluginKey) else {
    assertionFailure("Missing Flutter registrar for \(pluginKey)")
    return
  }

  pluginClass.register(with: registrar)
}

The project is still based in CocoaPods, so I had to filter the pods per target. which can get highly codebase specific, but the main idea is to have a function with shared pods, and then add specific pods to the targets.

def install_flutter_pods(excluded_plugins = [])
  # 1. Get the full list of Flutter plugins from the project
  all_plugins = flutter_get_plugin_list()

  all_plugins.each do |plugin|
    # 2. Check if we should skip this plugin for the current target
    next if excluded_plugins.include?(plugin.name)

    # 3. Handle modern Swift Package Manager (SPM) check
    # Skip if the plugin is already being handled by SPM
    next if plugin.has_swift_package?

    # 4. Tell CocoaPods to include this plugin as a dependency
    pod plugin.name, path: plugin.ios_path
  end
end

Flutter is already migrating to the Swift Package Manager, but the support is still evolving and CocoaPods is still used as a fallback for plugins that do not support Swift Package Manager yet.

Afterwards, I had to link the binaries to the correct targets and link the proper implementations to proper targets. At the beginning it was a bit of mess, but after clean up and creation of proper schemes to run two different targets out of my VS Code, it was working quite well.

Conclusion

Hopefully, this will help someone to navigate through the dependency hell of iOS and Android when you have different flavors and schemes.

It is annoying, but when you know what to look for, you will definitely manage it.

Keep in mind, that it is not a Flutter issue, but an issue of the underlying platforms, so you need to understand how they work to be able to solve it.

Socials

Thanks for reading this article!

For more content like this, follow me here or on X or LinkedIn.

Subscribe for more
LinkedIn GitHub Medium X