Compose Multiplataforma VS. Flutter

Tempo de leitura: 13 minutes

Hoje, daremos uma olhada no Compose Multiplatform para desenvolvimento entre plataformas e o compararemos com outro framework famoso do Google, o Flutter. Mas, antes de tudo, uma breve visão geral de cada tecnologia para aqueles que talvez não estejam familiarizados com elas.

O Flutter é uma estrutura do Google para desenvolvimento entre plataformas, cuja versão estável foi lançada em 2018. Ele oferece suporte ao desenvolvimento para iOS, Android, Web, Windows, Linux e Mac. Além disso, ele usa o Dart como linguagem de programação, que também é um desenvolvimento interno do Google (mais sobre isso adiante).

O Compose Multiplatform também é uma estrutura para desenvolvimento entre plataformas. Enquanto a estrutura da interface do usuário é desenvolvida pelo Google, a linguagem de programação, Kotlin, bem como a parte multiplataforma (KMP) são desenvolvidas pela JetBrains. Ele é compatível com plataformas como Android, iOS (Alpha), Windows, MacOS, Linux e Web (Experimental).

Vamos tentar comparar os vários aspectos dessas duas tecnologias tão diferentes, mas tão semelhantes, e selecionar o vencedor em cada categoria. As classificações e categorias serão subjetivas e nada científicas, mas devem ser interessantes. Além disso, vamos nos concentrar no desenvolvimento móvel, pois esse formato é o mais comum.

Vamos lá!

 

Configuração

Há guias de configuração bem escritos para o Flutter e o Compose Multiplatform. Para cada uma dessas tecnologias, o IDE principal é o Android Studio. Para o Flutter, o plug-in do Flutter deve ser instalado. Para iOS – xcode. Não tive problemas em nenhum dos casos. No entanto, o guia do Compose Multiplatform afirma que a versão mais recente do xcode pode não funcionar e você terá que instalar uma das versões mais antigas. No meu caso, não tive problemas com o xcode 15.

Aqui há um empate:

Flutter – 1
Compose Multiplatform – 1

 

Arquitetura da estrutura

Flutter

O núcleo do Flutter é o Engine, desenvolvido em C/C++. Ele é responsável por desenhar gráficos (Impeller para iOS e em Preview para Android, Skia para outras plataformas), operações de E/S, tempo de execução do Dart, cadeia de ferramentas de compilação e outros.

O Flutter Engine é empacotado no aplicativo junto com o código Dart e aumenta o tamanho (cerca de 3-4 MB para Android e 10 MB para iOS). A versão de lançamento usa a compilação AOT. A versão de depuração usa o Dart VM, que permite o recarregamento a quente e a operação do depurador. A versão de depuração usa a compilação JIT.

 

Compose Multiplataforma

O Compose Multiplatform é baseado no Jetpack Compose do Google e no KMP da JetBrains.

A principal diferença em relação ao Compose usual são os alvos KMP adicionais e as alterações correspondentes. Devido a essas diferenças, é possível não apenas escrever código Kotlin entre plataformas, mas também usar o Compose UI em diferentes plataformas.

O Compose Multiplatform, como o Flutter, usa o Skia para renderização no Android e o Skiko para outras plataformas (Skia para Kotlin).

 

Conclusão

Alguns pontos devem ser observados:

O que ambos os frameworks têm em comum é que o Flutter e o Compose Multiplatform não usam widgets nativos da plataforma como o React Native. Em cada caso, a estrutura desenha independentemente todos os componentes do zero usando o Skia ou análogos. Considero isso uma vantagem.

Vale a pena considerar a arquitetura em si em termos de afetar diretamente o desenvolvimento de aplicativos. Com o Flutter, tudo é simples: escrevemos o código em Dart e ele funciona em qualquer plataforma compatível. Se necessário, escrevemos código específico da plataforma, conectamos ao Dart por meio de um canal ou usamos plug-ins preparados pela comunidade.

Com o Compose Multiplatform, as coisas são um pouco mais complicadas. Aqui usamos o Kotlin Multiplatform. Para alguém que ouve falar do Compose Multiplatform pela primeira vez, pode ser enganoso pensar que se trata de uma tecnologia mágica que permite que você simplesmente reconstrua seu aplicativo Android no Compose e ele será executado sem problemas no iOS.

Mas isso não é totalmente verdade, e há uma série de limitações com as quais você terá que trabalhar.

Além das alterações na estrutura do projeto, você também precisará revisar o código e as dependências. O código que depende de pacotes Android ou Java só funcionará se você o deixar como específico para Android, e isso não é exatamente o que queremos de um aplicativo multiplataforma.

As mesmas restrições se aplicam a bibliotecas de terceiros. Falaremos mais sobre bibliotecas mais adiante.

Nessa categoria, eu prefiro o Flutter. A situação atual com o Compose Multiplatform não é ideal e pode ser difícil para iniciantes.

Flutter – 2
Compose Multiplatform – 1

 

Bibliotecas de terceiros

Flutter

Para o Flutter, há um ótimo site pub.dev com muitas bibliotecas (pacotes) para o Flutter e o Dart. Aqui podemos ver os pacotes populares recomendados pela equipe do Flutter e também há uma busca conveniente. Além disso, a comunidade do Flutter é bastante ativa e você pode encontrar bibliotecas para qualquer necessidade.

Além disso, há pacotes oficiais do Google para integração com o Firebase.

 

Compose Multiplataforma

Um número significativo de bibliotecas do Google não funcionará (se não todas), portanto, você terá que usar bibliotecas de terceiros para quase tudo, como nos bons velhos tempos. Por exemplo, a navegação de composição, o modelo de exibição ou a sala não são compatíveis. Mas, ao mesmo tempo, muitas bibliotecas populares podem ser usadas com a Multiplataforma, como Koin, Kodein, Realm e SQLDelight.

Além disso, há bibliotecas compatíveis com o KMP para navegação e MVVM/MVI. O problema é que um grande número de bibliotecas escritas para Android ou Java não funcionará devido às limitações mencionadas na seção “arquitetura”. Obviamente, se estiver escrevendo um projeto do zero, e não convertendo um projeto existente, o problema não será tão perceptível e você poderá selecionar imediatamente as bibliotecas correspondentes. Não há nada como o pub.dev, mas há uma boa página no github com uma lista de bibliotecas que funcionam com o KMP.

Vitória para o Flutter.

Flutter — 3
Compose Multiplatform — 1

 

Linguagem de programação

Flutter: Dart

O Dart é um desenvolvimento interno do Google, que deve ser uma alternativa ao JavaScript. A primeira versão não tinha tipagem forte. O Dart moderno tem um sistema de tipagem forte opcional e suporte à segurança nula. Ele oferece suporte à programação funcional e orientada a objetos.

Em 2023, a atualização do Dart 3 foi lançada, acrescentando os modificadores de classe de interface, classe de base, classe final e classe selada. Para a programação assíncrona, há um mecanismo async/ await. Para a programação reativa, há o Streams e a biblioteca rxdart. Em geral, a linguagem não é ruim, semelhante ao Java, mas com alguns recursos. Ela não é tão lacônica quanto o Kotlin, tem uma sintaxe mais inchada e não tem um recurso tão pequeno, mas importante, como as classes de dados.

Um exemplo de uma classe simples e anulabilidade (essa classe não tem uma implementação de equals, hashCode e toString; para isso, você precisa usar bibliotecas, por exemplo, esta ou esta):

abstract interface class Named {
  String get name;
}

final class Person implements Named {
  final String firstName;
  final String lastName;

  Person({required this.firstName, required this.lastName});

  @override
  String get name => '$firstName $lastName';
}

void checkPersonName() {
  Person? person = Person(firstName: 'Tobey', lastName: 'Maguire');
  debugPrint(person.name);
  person = null;
  debugPrint(person?.name);
}

 

Compose Multiplataforma: Kotlin

O Kotlin foi desenvolvido pela JetBrains e, no início, a linguagem deveria ser uma alternativa ao Java. Atualmente, Kotlin é a principal linguagem de programação para Android e pode ser compilada não apenas para bytecode JVM, mas também para binários nativos ou JavaScript. Ela suporta tipagem forte, segurança nula, tem suporte para programação orientada a objetos e funcional, bem como interoperabilidade com Java e Objective-C. Para programação assíncrona, há um mecanismo de corrotinas. Para reativa – Flow.

O Kotlin tem muito açúcar sintático agradável e uma sintaxe muito lacônica.

Vamos dar uma olhada no exemplo de código Kotlin que faz quase a mesma coisa que o código Dart. Aqui, também temos uma classe de dados, o que significa que equals, hashCode e toString são gerados automaticamente.

interface Named {
         val name: String
     }

     data class Person(val firstName: String, val lastName: String) : Named {
         override val name: String
             get() = "$firstName $lastName"
     }

     fun checkPersonName() {
         var person: Person? = Person(firstName = "Tobey", lastName = "Maguire")
         println(person?.name)
         person = null
         println(person?.name);
     }

Conclusão

Ambas as linguagens são bastante convenientes, fáceis de aprender e têm vários recursos modernos.

Mas, com base em minha experiência pessoal, programar em Kotlin é mais agradável do que em Dart. O Dart fornece ferramentas para escrever código limpo, tem suporte para programação assíncrona e reativa pronta para uso, mas algumas soluções parecem estranhas para mim, e a sintaxe às vezes é desajeitada em comparação com o Kotlin.

Flutter — 3
Compose Multiplatform — 2

 

Estrutura da IU

Ambas as estruturas seguem os paradigmas da interface do usuário declarativa. É claro que há muitas nuances e diferenças, e você pode facilmente escrever um artigo separado sobre elas. Aqui, vamos dar uma olhada nos blocos de construção básicos que usaremos para construir nossa UI, o que nos dará uma ideia aproximada de como trabalhar com estruturas em geral.

Para o Flutter, trata-se de um widget, para o Compose, de uma função composta.

Vamos tentar implementar um widget simples que terá vários campos de texto e um estado interno que pode ser alterado ao clicar em um botão.

Widget com estado do Flutter

O Flutter faz distinção entre widgets Stateless e Stateful como classes separadas. No caso de um widget Stateful, você precisa implementar duas classes: o widget em si e a classe State:

class CounterWidget extends StatefulWidget {
  final Color backgroundColor;
  final Color borderColor;

  const CounterWidget({
    super.key,
    required this.backgroundColor,
    required this.borderColor,
  });

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        const Padding(
          padding: EdgeInsets.only(bottom: 16),
          child: Text('Flutter running on iOS'),
        ),
        const Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        InkWell(
          customBorder: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
          onTap: () {
            setState(() {
              _counter++;
            });
          },
          child: Ink(
            width: 100,
            height: 32,
            decoration: BoxDecoration(
                color: widget.backgroundColor,
                border: Border.all(color: widget.borderColor, width: 2.0),
                borderRadius: BorderRadius.circular(8)),
            child: const Center(child: Text('INCREMENT')),
          ),
        )
      ],
    );
  }
}

Algumas coisas a mencionar:

Os parâmetros podem ser passados por meio do construtor para a classe State, mas isso não é necessário, e os campos da classe Widget podem ser acessados por meio do campo widget.

Além disso, quando se trabalha com UI, os widgets são usados para muitas coisas básicas.

Deseja adicionar preenchimento? Você precisa do widget Padding. Quer tornar algo clicável? Use o InkWell + Ink (essa combinação dá um efeito de ondulação) ou o GestureDetector. BoxDecoration é usado para adicionar uma borda com bordas arredondadas. O valor do contador em si está na classe CounterWidgetState, e podemos alterá-lo usando o método setState. Isso alterará o próprio estado e chamará o método de construção novamente.

Resultado

Testing in iOS

 

 

Funções compostas

O Compose Multiplatform também faz distinção entre os conceitos de stateful e stateless. Em ambos os casos, usamos uma função anotada com @Composable. A única diferença é se lembramos o estado usando a função remember ou não.

@Composable
    fun Counter(backgroundColor: Color, borderColor: Color) {
        var counter by remember { mutableStateOf(0) }

        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Text(
                modifier = Modifier.padding(bottom = 16.dp),
                text = "Compose Multiplatform running on iOS"
            )
            Text(
                text = "You have pushed the button this many times:"
            )
            Text(
                "$counter", style = MaterialTheme.typography.h4
            )
            val shape = RoundedCornerShape(8.dp)

            Box(contentAlignment = Alignment.Center,
                modifier = Modifier.size(width = 120.dp, height = 32.dp)
                    .border(width = 2.0.dp, color = borderColor, shape = shape)
                    .background(color = backgroundColor, shape = shape)
                    .clip(shape = shape)
                    .clickable {
                        counter++
                    }
            ) {
                Text("INCREMENT")
            }
        }
    }

Principais diferenças.

Estamos trabalhando com uma função Composable e, para torná-la stateful, tudo o que precisamos é de uma string.

var counter by remember { mutableStateOf(0) }

Em seguida, trabalhamos com o contador como um campo normal, atualizamos o valor e a parte correspondente da interface do usuário é redesenhada. Somente objetos imutáveis devem ser lembrados; a modificação de um objeto mutável não levará à recomposição. A estrutura do widget é muito semelhante à que tínhamos com o Flutter, mas, nesse caso, algumas coisas são obtidas com modificadores em vez de widgets individuais. Observe Modifier.padding e Modifier.clickable. A borda também é implementada usando Modifier.border.

Resultado

Conclusão

Embora o código de implementação seja significativamente diferente, ambas as estruturas compartilham uma filosofia semelhante. E, sim, você tem que escrever mais código no Flutter, mas o motivo, em parte, não está na estrutura em si, mas na linguagem Dart.

Ambas são estruturas de UI declarativas modernas e fáceis de usar, o que resulta em um empate.

Flutter — 4
Compose Multiplatform — 3

 

Comunicação com a plataforma

Quando se trata de desenvolvimento entre plataformas, mais cedo ou mais tarde você terá de lidar com a necessidade de se comunicar de alguma forma com a API nativa da plataforma. Vamos ver o que o Compose Multiplatform e o Flutter oferecem para isso.

Flutter

O Flutter tem um mecanismo de canais de plataforma que permite que você troque tipos de dados primitivos com código nativo (Swift ou Kotlin, no nosso caso).

O mecanismo funciona de forma assíncrona para não bloquear a interface do usuário.

Adicione um canal ao Flutter:

const platform = MethodChannel('samples.flutter.dev/platform');

Future<void> _getPlatform() async {
  final platformName = await platform.invokeMethod<String>('getPlatform') ?? '';
  setState(() {
    _platformName = platformName;
  });
}

Android:

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "samples.flutter.dev/platform"
        ).setMethodCallHandler { call, result ->
            if (call.method == "getPlatform") {
                result.success(getPlatform())
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getPlatform(): String {
        return "Android ${android.os.Build.VERSION.SDK_INT}"
    }
}

iOS:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      
  let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
      
      let platformChannel = FlutterMethodChannel(name: "samples.flutter.dev/platform",
                                                binaryMessenger: controller.binaryMessenger)
      platformChannel.setMethodCallHandler({
        (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
          guard call.method == "getPlatform" else {
              result(FlutterMethodNotImplemented)
              return
            }
            self.getPlatform(result: result)
      })
      
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
  private func getPlatform(result: FlutterResult) {
    let platform = UIDevice.current.systemName + " " + UIDevice.current.systemVersion
      result(platform)
  }
}

O mecanismo é simples, ele faz seu trabalho, mas, como podemos ver, você precisa escrever código em três idiomas diferentes.

Além disso, o Flutter oferece a capacidade de incorporar visualizações nativas do iOS e do Android. Isso também funciona de forma inversa, e é possível incorporar o Flutter em projetos nativos existentes do iOS e do Android.

Vale a pena observar que, em muitos casos em que você precisa trabalhar com código nativo ou widgets nativos, já existem bibliotecas oficiais ou não oficiais criadas.

Por exemplo, um widget do Flutter para trabalhar com o Google Maps ou uma biblioteca para trabalhar com notificações locais.

 

Compose Multiplataforma

A principal vantagem aqui é que você pode se comunicar com a API nativa usando o Kotlin. O KMP fornece um mecanismo de expectativa/realização que é fácil de usar.

Por exemplo, criamos uma classe expect no módulo comum:

// KMP Class Definition
expect class Platform() {
    val name: String
}

Classe real no módulo iOS que usa a API do iOS:

// iOS
actual class Platform actual constructor() {
    actual val name: String =
        UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

Classe real no módulo Android que usa a API do Android:

// Android
actual class Platform actual constructor() {
    actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

É isso, agora podemos usar a classe Platform como uma classe normal, a implementação específica será selecionada automaticamente dependendo da plataforma.

Também há interoperabilidade com o objective-C.

Com relação ao uso de widgets do Compose junto com widgets nativos, a situação é a seguinte.

Android – a maioria dos widgets no Jetpack Compose e no Compose Multiplatform é idêntica. Tudo é simples aqui, você só precisa modificar os locais com código específico do Android. Por exemplo, você pode usar a biblioteca moko para acessar recursos.

O Compose Multiplatform também é compatível com o UIKit e o SwiftUI.

 

Conclusão

O Compose vence, graças ao poder da multiplataforma Kotlin, pois podemos nos comunicar com a API nativa usando uma linguagem de programação.

Flutter — 4
Compose Multiplatform — 4

 

Ferramentas

Flutter

Trabalhando com a interface do usuário

Não há recurso de visualização, portanto, alteramos o código e damos uma olhada no emulador. Esse problema é parcialmente compensado pelo fato de o Flutter ter um hot reload rápido e funcional. Alteramos nosso widget, salvamos o arquivo e as alterações são refletidas no emulador imediatamente.

Criação de perfil

Há uma ferramenta muito útil, o Flutter Dev Tools. Ela mostra a estrutura da interface do usuário, as chamadas de rede e o desempenho do aplicativo. Além disso, ela é executada no navegador, o que também é uma vantagem, pois se você quiser usar o VS Code em vez do AS, poderá fazer isso facilmente.

Depurador

Ele existe e funciona. A única coisa que funciona é que é horrível. Demora muito para se conectar, trava, salta para linhas aleatórias ou para em pontos de interrupção excluídos anteriormente. Se o seu aplicativo funcionar bem, depois de conectar o depurador, ele poderá simplesmente congelar ou travar.

Geralmente, é mais fácil adicionar o maior número possível de registros do que conectar um depurador.

 

Compose Multiplataforma

Trabalhando com a interface do usuário

O Jetpack Compose tem um recurso muito legal: a anotação @Preview. Ela permite que você veja imediatamente a aparência do widget em diferentes telas do IDE.

O problema é que o @Preview não funciona com o Compose Multiplatform e só pode ser adicionado a funções compostas no módulo Android. Em teoria, você pode ter funções compostas no módulo comum e adicionar wrappers simples com a anotação @Preview no módulo Android, mas mesmo com essa abordagem há um problema no momento.

Talvez um hot reload ajude?

Bem, não. No Android, isso funciona, mas, na verdade, você ainda precisa reiniciar a atividade. No iOS, essa possibilidade não existe e você precisa remontar e reiniciar o aplicativo todas as vezes.

O depurador funciona no iOS e no Android, mas não o testei em um projeto grande.

No Android, o criador de perfil funciona no Android Studio.

Os instrumentos xcode devem funcionar no iOS. Não consegui conectá-lo, mas tudo funcionou na apresentação.

Conclusão

Por enquanto, o desenvolvimento no Flutter é um pouco mais conveniente, mas, eventualmente, isso pode mudar em favor do Compose Multiplatform.

Flutter — 5
Compose Multiplatform — 4

 

Maturidade

O Flutter 1.0 surgiu em 2018. Durante esse tempo, ambas as estruturas se estabilizaram e obtiveram novos recursos, e agora podem ser recomendadas para uso em grandes aplicativos de produção. Há histórias de sucesso nesta página. Além disso, há uma grande comunidade que sempre ajudará com problemas e dúvidas e um grande número de bibliotecas de terceiros.

Gostei de usar o Compose Multiplatform porque gosto do Compose e do Kotlin. Mas devo admitir que ele é muito bruto para o desenvolvimento móvel entre plataformas, e os desenvolvedores falam abertamente sobre isso.

Aqui está uma captura de tela da apresentação de maio de 2023:

Aqui temos um ponto para o Flutter.

Flutter — 6
Compose Multiplatform — 4

 

Resultado final

Flutter – 6
Compose Multiplatform – 4

O Flutter tem suas falhas, mas é uma tecnologia madura e pronta para uso que você pode selecionar com confiança para o desenvolvimento multiplataforma móvel.

Compose Multiplatform – é uma tecnologia muito promissora, e o Google já está trabalhando no suporte ao Compose Multiplatform para o Jetpack. Em alguns anos, o Compose Multiplatform poderá se tornar um forte concorrente do Flutter, o que é surpreendente, pois ambos são parcial ou totalmente desenvolvidos pelo Google. Talvez haja um recém-chegado no cemitério de produtos do Google.

Mas, agora, eu usaria o Compose Multiplatform apenas para experimentos ou projetos de estimação.