42 – Probando GoMobile
Introducción
Go tiene un paquete llamado Gomobile que me llamó mucho la atención el fin de semana. Tiene como objetivo ser una herramienta con dos fines específicos:
- Crear aplicaciones completas tanto para dispositivos iOS como para dispositivos Android.
- Crear bibliotecas compatibles para desarrollo en ambas plataformas.
El primer grupo es algo similar a lo que ofrecen otras herramientas para construir aplicaciones híbridas como Flutter, ReactNative o Ionic. El segundo me pareció un poco más interesante. Poder crear bibliotecas que se utilicen tanto para Android como para Iphone y reutilizar ese codigo que podría usarse para implementar la lógica del negocio (BL) en un solo lugar.
Asi, decidí explorar este camino e implementar una simple aplicación que utilice GoMobile para hacer peticiones HTTP a un servidor de prueba usando Go y llamar estas funciones desde una aplicación nativa de iOS hecha en SwiftUI.
N.B.: A lo largo de este tutorial van a haber algunos "gotchas", basicamente cosas raras que tienes que tener en cuenta sino no va a funcionar, cada vez que se presente una la indicare con la palabra "Gotcha":
Para el siguiente blog se usaron las siguientes dependencias:
- MacOS (v11.4)
- Editor de Textos - (Visual Studio, vim, EMacs)
- Xcode (v13.2.1)
- Go (v1.16)
Setup
Primero, para usar la platafroma tenemos que tener instalado Go (almenos la version 1.16)
|
|
go version go1.16 darwin/arm64
Una vez instalada las dependencias, crearemos la siguiente estructura:
gomobile-test/
├── framework/
├── go/
└── ios/
- En
framework
se encontrará el compilado que gomobile va a generar - En
ios
ubicaremos el proyecto de SwiftUI de ejemplo, y, - en
go
ubicaremos el codigo fuente en Go que usaremos como base para el demo.
Para crear la estructura podemos usar el siguiente comando:
|
|
Una vez hecho esto, navegamos a nuestra carpeta de Go e iniciamos un nuevo módulo
|
|
Finalmente, necesitamos instalar el paquete de gomobile
y su dependencia gobind
:
|
|
Crear la biblioteca
A continuación crearemos una biblioteca básica, creamos un archivo main.go
y agregamos el siguiente contenido:
|
|
GOTCHA
Las funciones que se exportan, al igual que en Go normal, tienen que empezar con una letra mayúscula.
GOTCHA2
El nombre del Package
, determina el nombre de la biblioteca.
Una vez hecho esto, abrimos una línea de comandos en la carpeta donde se encuentra nuestro archivo y vamos a compilarlo para iOS usando la siguiente función:
|
|
GOTCHA
para compilar para arquitecturas iOS es necesario correr el comando en una Mac.
Una vez hecho esto, si abrimos nuestra carpeta Framework, veremos que se ha creado una serie de archivos, tómate un tiempo para explorar algunos de los archivos generados por gomobile
.
framework
└── GMTest.xcframework/
├── Info.plist
└── ios-arm64/
└── GMTest.framework/
├── Headers -> Versions/Current/Headers/
├── Modules -> Versions/Current/Modules/
├── MyTest -> Versions/Current/MyTest
├── Resources -> Versions/Current/Resources/
...
Una vez que hayamos generado la biblioteca, tenemos que crear un proyecto de XCode en la carpeta iOS, para poder importar la biblioteca, veamos como hacerlo.
Crear el proyecto en SwiftUI
Primero, abrimos Xcode y creamos un nuevo proyecto, lo llamaremos "GMTest-ios", (el nombre en realidad no afecta), lo crearemos con Switf UI pero esto tambien funciona con Swift puro (y obviamente con Obj-c)
Instalar la biblioteca
Ahora, abriremos nuestro explorador de archivos "Finder" y jalaremos la carpeta generada en framework
a nuestro proyectos, Aqui podemos des-seleccionar la opcion de "copiar elementos si necesario" para que cada vez que hagamos un build de nuestra biblioteca automáticamente XCode la reconozca y no tengamos que estar agregándola cada vez. Esto es para que XCode "encuentre" la referencia a la biblioteca al momento de hacer nuestro "build".
Una vez importado, abrimos nuestro ContentView.swift y en la parte de arriba importaremos el framework
|
|
Crear una vista simple en SwiftUI
Primero probaremos la función SayHello
. Para esto reemplazaremos el "Hello, World" que viene por defecto en Swift por una llamada a nuestra función:
|
|
Corremos esto y deberiamos ver "Hello, World!!!!" en nuestro dispositivo.
Ahora hagamos esto un poco más interesante: agregaremos un boton, un string de @State
y un texto
|
|
Dentro de la acción del botón agregaremos la llamada a nuestra función y asignaremos a la variable message
el resultado de ésta.
struct ContentView: View {
@State var message = "..."
var body: some View {
VStack{
Text(message)
.padding()
Button("Decir algo", action: {
+ message = GMTestSayHello("World!!")
})
}
}
}
Manejar el "estado" con la biblioteca
Ahora haremos algo mas interesante, agregarle un sencillo contador a nuestra variable, modificamos nuestro codigo de Go:
package GMTest
+ import (
+ "fmt"
+ )
+
+ var counter = 0
func SayHello(name string) string {
- return "Hello, " + name;
+ counter++
+ return fmt.Sprintf("Hello, %s, %d", name, counter)
}
Ahora compilamos otra vez nuestra biblioteca:
|
|
Volvemos a correr el build en nuestro XCode, y apretamos varias veces el botón, veremos que el estado también se actualiza constantemente.
Ahora vayamos un poco más profundo, haremos una petición web y retornaremos el resultado, aquí nos toparemos con nuestro primer gran desafío.
Hacer peticiones desde go
Usaremos el paquete net/http
para poder hacer las peticiones
|
|
Esta función retornará un string
con el contenido del Dog Facts API, podemos llamarla en SwiftUI desde nuestro botón de la siguiente manera:
# ContentView.swift
# ...
- message = GMTestSayHello("World!!")
+ message = GMTestGetExampleAPI()
# ...
Manejar JSON desde Go
Un string
es útil pero idealmente quisieramos retornar solo el Fact del perro, no todo lo que regresa el API. Para hacerlo, usaremos el paquete encoding/json
|
|
Y actualizamos el proyecto de Swift UI para llamar a la nueva función
# ContentView.swift
# ...
- message = GMTestGetExampleAPI()
+ message = GMTestGetDogFact()
# ...
Compilamos la biblioteca y compilamos el proyecto de Swift y obtendremos lo siguiente:
Conocemos a nuestro némesis
Ahora, que tal si en vez de retornar un simple string
queremos retornar todo el objeto DogFact, y en Swift elegir qué visualizar? Si intentamos hacerlo, nos encontraremos con la desafortunada realidad que no es posible. Esto es debido a la principal limitante de esta biblioteca: solo soporta algunos tipos de dato primitivos. La lista de datos soportados se detallan a continuacion:
Tipos de dato soportados por Gomobile (a traves de Gobind):
- Signed integer and floating point types.
- String and boolean types.
- Byte slice types. Note that byte slices are passed by reference, and support mutation.
Además:
- Toda funcion cuyos valores de retorno y parametros esten incluidos en la lista anterior. Las funciones deben retornar cero, uno o dos resultados, donde el segundo resultado es de tipo "error"
- Toda interface, cuyos metodos exportados sean del tipo soportado
- Todo -struct- cuyos metodos exportados sean funciones compatibles y cuyos campos exportados sean del tipo soportado
Que significa
Esto significa que lamentablemente no podemos usar una variedad de tipos de dato, sobretodo Slices
(como arreglos en Go) y tipos definidos por el usuario como nuestro Struct
de DogFact
en sí.
Según la documentacion de gobind
esto un Work In Progress:
The set of supported types will eventually be expanded to cover more Go types, but this is a work in progress.
Este mensaje está así desde 2015, ojalá que pronto hayan novedades, pero no podemos esperar que sea asi. De todas formas, hay una forma de superar el inconveniente de los Slices
, detallado a continuación.
Superar (algunas de) las limitaciones
Mientras, podemos hacer un pequeño truco para poder saltar este problema, utilizando una interfaz. Vamos a crear un receptor y se lo vamos a pasar como argumento en la función que llamemos desde Swift, veamos como:
Crear la interfaz de receptor
Creamos una intrafaz llamda DogFactReceiver
, en Go, cuyo único método será un metodo Add
con un parámetro de tipo DogFact:
# main.go
...
+ type DogFactReceiver interface {
+ Add(dogFact string)
+ }
...
N.B.: En este caso, estamos usando un parámetro de tipo string pero también funcionaría con otro tipo, como un struct.
Luego, definimos una función de ayuda (helper) que reciba la función que acabamos de definir, y llamamos a su método Add:
|
|
En Swift, necesitamos crear una clase Receiver
que implemente el protocolo que hemos definido en nuestra biblioteca.
|
|
Luego necesitamos crear una instancia de esta clase, la cual usaremos para pasar nuestros datos cuando llamemos la funcion. Además, agregaremos la llamada a nuestra nueva función y mostraremos sólamente el último mensaje recibido:
|
|
De esta manera, podemos pasar cualquier Slice de Go a un Array en Swift. Incluso, podemos pasar Structs, sin embargo, solo se exportarán los campos del struct que sean compatibles con los tipos de datos soportados.
Conocer las limitaciones insuperables (por el momento)
De todas maneras, existen limitaciones más grandes, que, por el momento no podemos superar, entre ellas:
- Usar tipos de datos no compatibles en Structs (sobre todo, punteros)
- Tiempo de compilado elevado, esto es dado que se descargan las dependencias cada vez que se ejecuta el bind
Conclusiones
Gomobile definitivamente es una gran herramienta. Nos permite escribir código en Go que puede ser reutilizado en proyectos iOS y Android. El problema principal para poder ser adoptado es entonces el soporte de los tipos de dato. Todo parece indicar que el desarollo del proyecto está atascado. Parece ser posible, sin embargo, poder hacer un Framework basado en gomobile, que supla estos defectos automáticamente, esto ayudaría a su vez a impulsar Golang como herramienta de desarollo móvil.
Cabe destacar que si bien es cierto hay una gran ventaja en reutilizar código entre plataformas, debemos notar que ésta no es una idea nueva. Por ejemplo, Dropbox hacía algo similar usando C y una biblioteca llamada djinni. Al final, Dropbox decidió abandonar el proyecto dado que, entre otras razones, mantener programadores C++ era más complejo que reescribir el código dos veces en ambas plataformas (Android y iOS).
Idealmente, Go es un lenguaje más moderno y más seguro en especail para principantes, y al mismo tiempo bastante poderoso, así que esperamos ansiosos a que se solucionen los problemas de esta herramienta para que en corto plazo podamos estar utilizando en WeMake el mismo lenguaje para generar tanto nuestros servicios como las las biblotecas compatibles para ambas plataformas.
Fuentes y lecturas recomendadas:
- [ENG] Calling Go code from Swift on iOS and vice versa with Gomobile | by Mat Ryer …
- [ENG] Instrucciones de Golang sobre gomobile , Mobile · golang/go Wiki · GitHub
- [ENG] gomobile command - golang.org/x/mobile/cmd/gomobile - pkg.go.dev
- [ENG] gobind command - golang.org/x/mobile/cmd/gobind - pkg.go.dev