Las tecnologías cross platform han mostrado ser una excelente opción al momento de desarrollar aplicaciones móviles, mejorando la velocidad del desarrollo, reduciendo costos y optimizando el uso de recursos. Por esta razón, con el ánimo de estar a la vanguardia y dar propuestas de valor a nuestros clientes, en Sofka hemos evaluado y realizado pruebas de concepto, entre estas, una para el retailer más importante del país. En ellas se propone desarrollar los módulos de nuevas funcionalidades con flutter, e integrar su desarrollo nativo de manera progresiva. A continuación, mostramos un paso a paso para lograr esta integración mencionada.
Integrar Flutter en IOS
Requisitos
- Flutter 3.3.0 o superior
- Xcode 13.2.1 o superior
- Un proyecto (App) creado previamente con Xcode Visual Studio Code o Android Studio
Creación del módulo Flutter
Para integrar flutter en IOS, primero se debe crear un módulo Flutter. Desde la línea de comandos, ubícate en tu carpeta de preferencia y ejecuta el siguiente comando:
Este comando crea un módulo Flutter llamado my_flutter en nuestra carpeta de preferencia.
Creando nuestro método de entrada
Abriremos la carpeta my_flutter con Visual Studio Code y editaremos el archivo main.dart con la siguiente línea de código encima del método main:
void showFlutter() => runApp(const MyApp());
Construcción de los XCFrameworks
Para generar los frameworks encargados de correr el módulo Flutter en IOS ejecutaremos el siguiente comando, ubicándonos en la raíz del proyecto:
flutter build ios-framework
Cada vez que realices cambios en el código del módulo Flutter tendrás que ejecutar el comando anterior.
Integrando los XCFrameworks en Xcode
Cuando se haya terminado de ejecutar el comando podremos encontrar dos frameworks llamados: App.xcframework y Flutter.xcframework ubicados en la ruta: my_flutter/build/ios/framework/debug, vamos a integrar estos dos frameworks en nuestra app nativa de IOS arrastrando y soltando las dos carpetas en el apartado de Frameworks, Libraries and Embedded Content de Xcode.
Agregando Flutter al AppDelegate
Una vez agregada nuestra librería, iremos a nuestro AppDelegate, importamos Flutter y crearemos la variable “engines” para gestionar nuestras FlutterEngines que se encargará de renderizar la interfaz de usuario de la aplicación usando Flutter.
import Flutter
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var engines = FlutterEngineGroup(name: “multiple-flutters”, project: nil)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> B // Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.Conne // Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: “Default Configuration”, sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } }
Agregando un FlutterViewController
Vamos a nuestro ViewController y agregaremos el siguiente código:
import Flutter
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Botón perzonalizado
let button = UIButton(type:UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle(“Show Flutter!”, for: UIControl.State.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
}
// Acción para ir al módulo de flutter
@objc func showFlutter() {
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).engines.makeEngine(withEntrypoint: “showFlutter”, libraryURI: ni let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}
}
Integrar Flutter en Android
PASOS:
- Creación del módulo Flutter.
Es importante tener en cuenta que desde el inicio el proyecto sea creado como un módulo, y que el nombre del paquete sea diferente al del proyecto nativo, debido a que si los nombres son iguales, se crearán conflictos a la hora de importar el módulo en nuestra app Nativa.
$ flutter create -t module –org com.example.fluttermodule flutter_module
- Verificar los requisitos para agregar el módulo Flutter:
En el build.gladle a nivel de app, agregar:
ndk {
abiFilters ‘armeabi-v7a’ , ‘arm64-v8a’ , ‘x86_64’
// Filtro para arquitecturas compatibles con Flutter.
}
}
El motor Flutter de Android utiliza características de Java 8. En el mismo build gradle, asegúrese de que la aplicación anfitriona de Android declare la siguiente compatibilidad de fuente:
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
- Generar la librería (AAR) del módulo Flutter.
Generar el archivo AAR, dentro del proyecto nativo, este creará también una carpeta a nivel de proyecto donde se guardará la librería AAR.
\AndroidStudioProjects\AppNativa\flutter_module
- Integrar el Módulo Flutter en el proyecto nativo.
Lo primero que se debe realizar es dirigirse al archivo settings.gradle (AppNativa) y cambiar el RepositoriesMode.FAIL_ON_PROJECT_REPOS a RepositoriesMode.PREFER_PROJECT .
Si se establece este modo, hará que el proyecto use los repositorios declarados por el proyecto.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
google()
maven Central()
}
}
Editar el build.gradle a nivel de proyecto de la aplicación host para que incluya el repositorio local y la dependencia:
Para mayor comodidad, el archivo será modificado a tipo kotlin, renombrándolo como builg.gradle.kts. Se incluirá el repositorio local a nivel de todo el proyecto, se han degradado las versiones de las librerías, ya que en la versión 7.3.1 actualmente generan conflictos.
id(“com.android.application”) version “7.2.1” apply false
id(“com.android.library”) version “7.2.1” apply false
id(“org.jetbrains.kotlin.android”) version “1.7.10” apply false
}
allprojects {
repositories {
google()
mavenCentral()
maven(url = “../flutter_module/host/outputs/repo”)
maven(url = “https://storage.googleapis.com/download.flutter.io”)
}
}
tasks.register(“clean”, Delete::class) {
delete(rootProject.buildDir)
}
Ahora se agregarán las dependencias al proyecto nativo, en el archivo llamado build.gradle nivel de app. Nota: la manera correcta de colocar las implementaciones es la siguiente
debugImplementation ‘com.example.fluttermodule.flutter_module:flutter_debug:1.0’
profileImplementation ‘com.example.fluttermodule.flutter_module:flutter_profile:1.0’
releaseImplementation ‘com.example.fluttermodule.flutter_module:flutter_release:1.0’
}
Indicamos que inicie con el perfil de debug, finalmente sincronizar para que reconozca los cambios realizados.
//..
profile {
initWith debug
}
}
buildFeatures {
dataBinding true
}
Agregar una pantalla Flutter a una aplicación de Android
Agregar FlutterActivity a AndroidManifest.xml
Flutter nos proporciona FlutterActivity para mostrar una experiencia de Flutter dentro de una aplicación de Android. Como cualquier otro Activity, FlutterActivity debe estar registrado en su AndroidManifest.xml. Agregue el siguiente XML a su AndroidManifest.xml archivo dentro de su tag application:
android:name=”io.flutter.embedding.android.FlutterActivity” android:configChanges=”orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode”
android:hardwareAccelerated=”true”
android:windowSoftInputMode=”adjustResize”
/>
- Implementación Avanzada del módulo flutter
En Android Studio actualmente no es posible importar múltiples módulos de Flutter ya que esto genera conflictos con las implementaciones. Esto, debido a que, los nombres de los paquetes serán los mismos y la api de Flutter_Activity tampoco nos permite instanciar varios módulos.
Ahora, modificaremos el código del proyecto nativo. Para poder controlar las diferentes pantallas que deseamos abrir, separaremos cada pantalla con una clase que extiende de “FlutterActivity”
Primero se creará un activity nuevo llamado “ModuleFlutterProductDetail.kt”
En este caso no se desea crear un archivo .xml para la Activity por lo que desmarcamos la casilla ”Generate a Layout File”
Optimizaciones y carga de cada paquete Flutter
Para optimizar el código, utilizaremos 2 estrategias la primera es FlutterEngineGroup, este nos permitirá compartir recursos para permitir que se creen más rápido y con menos memoria.
La segunda Estrategia es emplear un precalentado o cargado en caché del módulo flutter.
No te preocupes si después de realizar esto la app pareciera que es lenta para abrir la primera vez un módulo, cuando firmes tu app la app será muy rápida. ????
Crear el FlutterEngineGroup el cual controlará varias páginas de flutter, se creará una sola clase para este y lo pondremos en el nivel más alto agregandolo en el AndroidManifest.xml
lateinit var engines: FlutterEngineGroup
override fun onCreate() {
super.onCreate()
engines = FlutterEngineGroup(this)
}}<manifest>
<application
android:name=”.App”
android:allowBackup=”true”
… > ************************************
</application>
</manifest>
Crea una clase para poder ejecutar en caché cada paquete (pre-calentar el código) para tener una carga rápida, además de indicar la página la cual queremos abrir.
fun onNext()
fun onBack()
}
class EngineBindings(activity: Activity, entrypoint: String, cacheId: String) {
val engine: FlutterEngine
init {
val app = activity.applicationContext as App
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), entrypoint
)
engine = app.engines.createAndRunEngine(activity, dartEntrypoint)
//se activa el canal de comunicación para cada página //En el siguiente paso generamos la clase de SettingMethodChannel para poder continuar.
SettingMethodChannel(engine)
FlutterEngineCache
.getInstance()
.put(cacheId, engine)
}
fun init() {}
}
Comunicación bidireccional.
Se creará un canal de comunicación bidireccional para poder pasar información del proyecto nativo al módulo de flutter y viceversa, para esto se creará una clase llamada SettingMethodChannel.
companion object {
lateinit var channel: MethodChannel
private lateinit var delegate: EngineBindingsDelegate;
var isStart: Boolean = false
@JvmStatic
fun detach() {
channel.setMethodCallHandler(null)
}
@JvmStatic
fun setDelegate(delegate: EngineBindingsDelegate) {
this.delegate = delegate
attach()
} private fun attach() {
channel.setMethodCallHandler { call, result ->
when (call.method) {
“next” -> {
Log.d(“NextPage”, “Startpage”)
this.delegate.onNext()
result.success(null)
}
“back” -> {
this.delegate.onBack()
result.success(null)
}
“getProductPageInfo” -> {
result.success(“Hola mundo :)”)
}
else -> {
result.notImplemented()
}
}
}
}
}
init {
channel = MethodChannel(
engine.dartExecutor.binaryMessenger,
“com.example.flutter_module”
)
attach()
isStart = true
}
}
Ahora modifica cada una de las activities que representan la página de cada módulo, en este caso dirígete a ModuleFlutterProductDetail.kt y heredar de FlutterActivity, de esta manera podremos controlar varias opciones y comportamiento de nuestro paginal flutter incluso poder tener control de su ciclo de vida.
import ..
class ModuleFlutterProductDetail : FlutterActivity(), EngineBindingsDelegate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
//se recupera la pantalla precargada en caché y se lo pasa como su proveedor
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return FlutterEngineCache.getInstance().get(“ModuleFlutterProductDetail”)
}
//se le pasa el canal de comunicación previamente creado,
// si este no existe se crea el canal de comunicación.
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
if (!SettingMethodChannel.isStart) {
SettingMethodChannel(flutterEngine)
}
SettingMethodChannel.setDelegate(this)
}
override fun onNext() {
startActivity(Intent(this, MainActivity::class.java))
}
override fun onBack() {
onBackPressed()
}
}
La clase MainActivity.kt será actualizada para que pueda ejecutar múltiples pantallas flutter desde esta clase.
import …
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
EngineBindings(
this,
entrypoint = “MainProductDetail”,
cacheId = “ModuleFlutterProductDetail”
).init()
with(binding) { //se debe crear un botón en el XML
buttonFlutter.setOnClickListener {
startActivity(
Intent(this@MainActivity, ModuleFlutterProductDetail::class.java)
)
}
}
}
}
Para que el canal de comunicación bidireccional funcione, este también tiene que ser creado en el paquete que se utilizará y ponerle el mismo nombre que tienen en android, en este caso se nombró como “com.example.flutter_module”, crea una clase en dart nombrada SettingMethodChannel donde puedes poner toda la lógica del canal de comunicación.
class SettingMethodChannel {
static const platform = MethodChannel(“com.example.flutter_module”);
Future<String> getProductPageInfo() async {
try {
return await (platform
.invokeMethod(‘getProductPageInfo’)
.then((value) => value));
} on PlatformException catch (exception) {
print(“Error en llamada al método getProductPageInfo”);
return “$exception”;
}
}
Future<void> didBackPressedAndroid() async {
try {
await (platform.invokeMethod(‘back’));
} on PlatformException catch (exception) {
print(exception);
}
}
Future<void> startNextPage() async {
try {
await (platform.invokeMethod(‘next’));
} on PlatformException catch (exception) {
print(exception);
}
}
}
La primer pagina: prouct_detail_page.dart
import ‘../../setting_method_channel.dart’;
class ProductDetailPage extends StatelessWidget {
ProductDetailPage({Key? key}) : super(key: key);
final SettingMethodChannel settingMethodChannel = SettingMethodChannel();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(“Product Detail Page”),
),
body: Center(
child: ElevatedButton(
child: Text(“Next Page”),
onPressed: () {
settingMethodChannel.startNextPage();
},
),
),
);
}
}
Se crean varios puntos de entrada y desde nuestro proyecto nativo elegiremos si queremos ejecutar la clase main, o alguna otra que hayamos creado.
Para realizar esto, dirigirse al archivo main.dart de tu módulo Flutter, y actualizar la clase.
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold(
body: Center(child: Text(“Hola Mundo”)),
),
);
}
}
/******************************************************************/
@pragma(“vm:entry-point”)
void MainProductDetail() async {
runApp(const StartProductDetail());
}
class StartProductDetail extends StatelessWidget {
const StartProductDetail({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: “/product_detail”,
routes: {
“/product_detail”: (_) => ProductDetailPage(),
},
);
}
}
Nuevamente cree los archivos AAR del módulo Flutter para actualizar los últimos cambios.
$ flutter build aar -output-dir\AndroidStudioProjects\AppNativa\flutter_module
Ejecutamos la app nativa.
Si quieres ver los tutoriales completos puedes visitar…
https://medium.com/@mvalenciaminota/como-integrar-flutter-en-una-app-nativa-ios-d5564bf320f7