Optimización en el peso de aplicaciones en Flutter
Tags: flutter, dart, productivity, performance
Published: 2022-07-10 17:34:32

Una de las cosas que siempre me preocupan al comenzar un proyecto es la optimización de una aplicación ¿Por qué? La optimización es una de las bases de la aplicación ya que es la que te va a permitir un correcto funcionamiento, menor cantidad de errores, posibilidad de agregar mas funciones, mayor porcentaje de accesibilidad, es decir, que puedan adquirir la aplicación mayor variedad de celulares ya sea por rendimiento o por su peso.

He estado probando e investigado distintas formas de optimizar tanto el rendimiento de una aplicación como su peso de forma conjunta, por lo tanto eso es lo que voy a explicar en esta ocasión de forma simple y con pruebas.

# Antes de comenzar
Vamos a dar un paseo por como inicia una aplicación, como aumenta su peso y como se puede disminuir junto con algunos tips.

## Peso inicial de la aplicación
Primero vamos a ver cual es el peso inicial de una aplicación flutter sin ningún tipo de optimización ni nada agregado a la misma:

1. Este es con el peso que una aplicación flutter comienza y posteriormente se hace una APK Build:
![build apk](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0pv5k359m09g3pnjbk9.PNG)

2. Este es con el peso que una aplicación flutter comienza y posteriormente se hace un appbundle Build:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yknk2a63jpka5511w4h.PNG)

## ¿Por que aumenta el peso de la aplicación?
Para poder encontrar una solución primero hay que saber el problema, y el problema son las razones de porque nuestra aplicación aumenta su peso. Entonces a modo de prueba agregue al proyecto 5 paquetes de Firebase (Firestore, Realtime Database, Auth, Core y Storage), una base de datos BaaS con bastante reputación en Flutter y utilizada por muchos desarrolladores y concluí que únicamente con los paquetes aumentó el peso de la aplicación en un **7.93%**:

![7.93%](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tryjwkvtb6cco4s8t6r.PNG)

Luego probé agregando muchos paquetes que utilizaría normalmente un E-commerce complejo con lo cual a partir del valor inicial, se obtuvo un **28.66%**, lo cual es demasiado para una aplicación en la cual no tiene nada mas que el contador por predeterminado que se le incluye a la aplicación al crearse y teniendo solo los paquetes agregados:

![28.66%](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sdfuhpw8rlktht9opuy.PNG)

Lo siguiente que hice fue pasar del Android SDK 20 que utilizaba la aplicación al SDK 29 por lo cual a partir del valor inicial se obtuvo ahora un porcentaje del **162.8%** ¡UNA LOCURA!:

![162.8%](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uog2q5pgq42xakoygm98.PNG)

Con esto ya vimos que con tan poco, el peso de la aplicación puede aumentar exponencialmente si no lo mantenemos controlado, entonces
sabiendo ya esto, comencemos.

#Optimizaciones

## 1. gradle.properties y app/build.gradle

Una de las cosas que note es el uso de estas 3 propiedades en aplicaciones que tocaron los 40MB:

```
android.enableR8=true //Se utiliza en gradle.properties
```
```
    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
        }
    } //se utiliza en app/build.gradle
```
Estas 3 propiedades se deben tener desde el inicio porque a medida que aumenta el contenido de la aplicación optimizara el peso de la Build.

**Android R8** utiliza las técnicas de proguard y las mejora por lo tanto hay que tener un archivo proguard-rules.pro, a continuación muestro algunas reglas que pueden ser de utilidad:
```
#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }
```
Nota: En el proyecto de prueba no agregue el proguard-rules.pro ya que no iba a tener ningún efecto en la aplicación tal como estaba y solo iba a aumentar el peso el cual normalmente aumenta en 0.1MB a la aplicación

**minifyEnabled** buscara eliminar todo símbolo que no sea de utilidad en el código marcándolo para que sea removido por shrinkResources.

**shrinkResources** buscara eliminar todo recurso que no sea de utilidad en la aplicación marcado previamente por minifyEnabled.

Estos 3 parámetros se recomienda utilizar juntos.

## 2. Parámetros en flutter build apk/appbundle

Crear una build directamente en flutter te permite de una forma fácil, poder agregar distintos parámetros a la build para que se adapte mas fácilmente a tus necesidades a diferencia de entrar al proyecto como si estuvieras desarrollando una aplicación para Android. Procedemos con los parámetros:

### --split-per-abi
Aclaración, este parámetro únicamente funciona para APK

![flutter build apk split-per-abi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2sl55pjs5v5b08nijs8j.PNG)

Lo que hace es separar el proyecto en 3 APK distintas para 3 distintos tipos de dispositivos: android-arm, android-arm64, o android-x64, recuerda que si no tienen la apk especifica que necesita cada celular, ese mismo celular no la podrá utilizar. Por ejemplo un celular que utiliza arm no podrá utilizar una apk que sea arm64.

En caso de que solo quieras una apk de las 3 ABI debes utilizar:
` flutter build apk --target-platform [ABI] `

![target platform](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8q8l5jr9yxhgtb4y18t.PNG)

### --obfuscate --split-debug-info=symbols/

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d40tb4ne54dhltu19fpp.PNG)

Lo que hace es ofuscar el código, lo cual disminuye el peso y previene la ingeniería inversa y elimina los símbolos de debug, es decir, los que se producen en el desarrollo.

## 3. Reducción de peso de los recursos utilizados
Los archivos utilizados en la aplicación como por ejemplo imágenes, videos, gif, json, svg, etc deben pesar muy poco. Tengo mi política de que pesen menos de 10KB cada archivo que este utilizando en la aplicación.

Talvez mas de uno se preguntara ¿Cómo lo hago? bueno, se puede utilizar cualquier pagina que se encargue de bajar el peso de los archivos. En mi caso que uso imágenes mas que nada, utilizo:

https://compresspng.com/es/

## 4. Utilizar archivos de internet
Otra forma de disminuir el peso de la aplicación que es mas eficiente que la reducción de peso de las imágenes es mostrar las imágenes desde una pagina web utilizando:

```
            Image.network("URL DE LA IMAGEN");
```

Obviamente que esto es muy dependiente de si tu aplicación utiliza o no el internet, pero es una alternativa muy viable, que hasta en el caso necesario puedes almacenarla en el cache para no estar requiriendo siempre una conexión a internet.

Esto no solo aplica a imágenes, sino también que puede aplicar también a fuentes de letra, por lo tanto se puede utilizar el paquete de google_fonts para traer esas fuentes que quieres desde  internet:

https://pub.dev/packages/google_fonts

## 5. Utilizar código propio
No es malo utilizar paquetes, a lo que me refiero es que los mismos paquetes como ya demostré mas arriba, pueden aumentar el peso de la aplicación porque puede contener cosas que no nos sirve como puede ser el caso de algún paquete que nos ponga a disposición variedad de iconos. 

En el caso de los iconos puedes utilizar:

https://www.fluttericon.com/

para crear un SVG del icono que quieras e insertarlo fácilmente a tu aplicación.

## 6. Desarmar los paquetes
Esto puede ser una tarea complicada en ciertos casos, por lo tanto recomiendo hacer solo esto en caso de que el paquete lo único que haga es facilitarte la creación de un widget.

Como todos los paquetes de pub.dev son open source puedes instalarlo en tu proyecto, quitarle lo que necesites y pasarlo directamente a tu proyecto, eliminando posteriormente el paquete de pubspec.yaml.

no te olvides de dejar en un comentario los créditos al creador en donde guardes ese código ;)

Si sabes que solo vas a utilizar una plataforma, prioriza si es que el paquete te lo permite, utilizar la modificación hecha por el mismo desarrollador que solo tenga la implementación para la plataforma de su aplicación.

Ejemplo:
Google Pay tiene la posibilidad de implementar con su paquete para  Android y IOS, pero en su GitHub también presenta la posibilidad de utilizar para una sola plataforma.

https://github.com/google-pay/flutter-plugin

## 7. Evitar las redundancias y repeticiones en el código.
Esto es un error que se suele cometer y es repetir el código ya sea por una función o por repetir widgets varias veces ya sea para distinguir de donde viene la función, si proviene de android, ios o web, para mostrar varias veces un mismo widget o cualquier otra cosa.

Vamos a pasarlo al caso practico:

Digamos que estamos distinguiendo tres formas distintas de subida de datos a la colección "Productos" de Firestore Cloud.
El error seria realizar:

```
PublicarProducto(){

if(tag=="Especial"){
      FirebaseFirestore.instance.collection("Productos").doc(id.random).set({
        "name":product.username,
        "price":product.price,
        "image":product.image,
        "desc": product.description,
        "cat":product.category,
        "quant":1,
        'ownername':owner.name,
        "ownertag":"Especial",
      });

}else if(tag=="Normal"){
FirebaseFirestore.instance.collection("Productos").doc(id.random).set({
        "name":product.username,
        "price":product.price,
        "image":product.image,
        "desc": product.description,
        "cat":product.category,
        "quant":1,
        'ownername':owner.name,
        "ownertag":"Normal",
      });
}else{
FirebaseFirestore.instance.collection("Productos").doc(id.random).set({
        "name":product.username,
        "price":product.price,
        "image":product.image,
        "desc": product.description,
        "cat":product.category,
        "quant":1,
        'ownername':owner.name,
      });
}
}
```
Como vemos, lo que estamos queriendo hacer es dependiendo del "tag" de la persona, subir el producto con un tag distinto, incluso hasta si no existe tal tag.
Esto esta mal realizar porque hay redundancia y se repite únicamente por 1 sola variable, por lo tanto habría que utilizar:

```
PublicarProducto(){
      FirebaseFirestore.instance.collection("Productos").doc(id.random).set({
        "name":product.username,
        "price":product.price,
        "image":product.image,
        "desc": product.description,
        "cat":product.category,
        "quant":1,
        'ownername':owner.name,
        if(tag!=null)"ownertag":tag,
      });
}
```
De esta forma compactamos el código y evitamos redundancias. Esta bien, no es mucho código como para que se note el peso o el coste en el rendimiento pero si lo aplicamos a una aplicación grande y este error se repite mucho, el cambio será muy notable.

También puede ocurrir que repitamos una estructura en Widget build por lo tanto lo que se debería hacer es crear un Widget aparte y lo llamemos desde distintas pantallas o múltiples veces.

Pasemos otra vez al caso practico:

Este seria el error:
```
            Column(children: [
              Container(
                child: Row(
                  children: [
                    Text("HOLA"),
                    SizedBox(height: 15,),
                    TextFormField(decoration: InputDecoration(hintText: "Nombre"),),
                    Text("SI")
                  ],
                ),
              ),
              Container(
                child: Row(
                  children: [
                    Text("ARE"),
                    SizedBox(height: 15,),
                    TextFormField(decoration: InputDecoration(hintText: "Nombre"),),
                    Text("YOU")
                  ],
                ),
              ),
              Container(
                child: Row(
                  children: [
                    Text("GONNA"),
                    SizedBox(height: 15,),
                    TextFormField(decoration: InputDecoration(hintText: "Nombre"),),
                    Text("LEVEL")
                  ],
                ),
              ),
              Container(
                child: Row(
                  children: [
                    Text("FORSEN"),
                    SizedBox(height: 15,),
                    TextFormField(decoration: InputDecoration(hintText: "Nombre"),),
                    Text("LEVEL")
                  ],
                ),
              ),
            ],),
```
Estamos repitiendo una y otra vez el mismo Container que contiene el Row con los 2 Text (Que tienen distintos textos) el SizedBox y el TextFormField. Entonces lo que vamos a hacer es:

Crear un Widget fuera del Widget build:
```
  Widget UnSoloWidget ({required String texto1, required String texto2}){
    return Container(
      child: Row(
        children: [
          Text(texto1),
          SizedBox(height: 15,),
          TextFormField(decoration: InputDecoration(hintText: "Nombre"),),
          Text(texto2)
        ],
      ),
    );
  }
```

Se pasaran los parámetros en cada uno y todos redireccionaran al mismo widget con distintos valores de tal manera que quedara:

```
UnSoloWidget(),
```

De esta manera al utilizar el widget se redireccionara al mismo, te ahorraras tiempo, espacio y rendimiento:

```
Column(children: [
      UnSoloWidget(texto1:"HOLA",texto2:"SI"),
      UnSoloWidget(texto1:"ARE",texto2:"YOU),
      UnSoloWidget(texto1:"GONNA",texto2:"LEVEL"),
      UnSoloWidget(texto1:"FORSEN",texto2:"LEVEL"),
    ],);

```

## 8. No dejar código basura o no utilizado
Es algo que nos pasa a muchos dejar este tipo de código desperdiciado por nuestro proyecto con tan solo la idea de que se utilizara mas adelante, por lo tanto ante la duda de si será o no utilizado o sino es el momento de utilizarlo lo mejor es comentarlo (con // ), de esta manera no se compilara en el APK. Esto se puede aplicar a paquetes que no estén en uso comentándolos en el pubspec.yaml o archivos enteros.

## Tips
Para identificar si tiene alguno de los problemas ya descritos puede analizar el peso de la aplicación realizando los siguientes pasos:

1. Crear una build de su proyecto con:

` flutter build apk --target-platform android-arm --analyze-size `

2. Ir a Dart DevTools:
![2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ienh7vsb608kmunb58vb.PNG)

3. Elegir subir un archivo:
![3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jp4r1vkme9srp9gaevz5.PNG)

4. Seleccionar o insertar el json generado anteriormente:
![4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1cl29reqx3xh78ybluly.PNG)

En el caso de que quieras leerlo con appbundle en vez de un APK, debes sumarle 0.1MB o 0.2MB al total del APK.

# Fin

Ya terminando, esta fue una explicación sobre porque a veces pesan tanto nuestras aplicaciones y como controlar su peso para que seamos mejores y mas eficientes en nuestro trabajo como desarrolladores de aplicaciones de Flutter. Es verdad que existen mas formas de optimizar aplicaciones pero estas son formas que pueden aplicar a casi cualquier proyecto.

Muchas gracias.

# DOCUMENTACIÖN Y FUENTES

https://docs.flutter.dev/deployment/obfuscate#obfuscating-your-app
https://developer.android.com/studio/build/shrink-code
https://developer.android.com/guide/app-bundle
https://www.youtube.com/watch?v=9D63S4ZRBls
https://stackoverflow.com/questions/49064969/flutter-apps-are-too-big-in-size
https://stackoverflow.com/questions/52428973/how-to-minify-a-flutter-app-with-proguard
https://docs.flutter.dev/perf/app-size
Imagen de balanza de la portada: https://www.freepik.es/vector-gratis/bascula-digital-sobre-fondo-blanco_16262850.htm#query=balanza&position=22&from_view=keyword