<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged kotlin</title>
<link>https://coderlegion.com/tag/kotlin</link>
<description>Powered by Question2Answer</description>
<item>
<title>⚒️Todas as funções do aplicativo!:</title>
<link>https://coderlegion.com/33225/todas-as-funcoes-do-aplicativo</link>
<description>&lt;h4 id=&quot;vers-o-do-app-&quot;&gt;Versão do app:&lt;/h4&gt;
&lt;p&gt;A &quot;UpdateActivity&quot; apresenta as informações da versão instalada do Mulher Amparada, utilizando os dados diretamente do aplicativo instalado.&lt;/p&gt;
&lt;p&gt;Ela exibe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ícone do app disfarçado&lt;/li&gt;
&lt;li&gt;Nome do aplicativo&lt;/li&gt;
&lt;li&gt;Versão instalada (&quot;versionName&quot;)&lt;/li&gt;
&lt;li&gt;Número da build (&quot;versionCode&quot;)&lt;/li&gt;
&lt;li&gt;Indicador visual de disponibilidade do sistema&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A versão e a build são obtidas diretamente dos metadados do APK, garantindo que as informações exibidas correspondam à versão realmente instalada no dispositivo.&lt;/p&gt;
&lt;h4 id=&quot;bot-o-de-p-nico-&quot;&gt;Botão de Pânico:&lt;/h4&gt;
&lt;p&gt;Botão de Pânico, com ligação ao 180 de forma direta no primeiro clique.&lt;/p&gt;
&lt;h4 id=&quot;prote-o-por-barulho-&quot;&gt;Proteção por Barulho:&lt;/h4&gt;
&lt;p&gt;Ative a proteção, faça barulho alto e ele liga para o 180.&lt;/p&gt;
&lt;p&gt;caso ocorra alguma falha ou o aparelho da usuária não possua o sensor necessário, o aplicativo utiliza o microfone como alternativa. Ao detectar um barulho alto, a proteção é acionada.&lt;/p&gt;
&lt;p&gt;&amp;gt;Sobre a proteção por palmas/barulho: Nota de Segurança: Uma vez ativada, a proteção permanecerá vigilante e reativará o microfone automaticamente após cada detecção e ligação pro 180. Isso garante que o aplicativo continue te protegendo caso a situação de risco persista. Para desligá-la por completo, você deve fazer isso manualmente no aplicativo após o término da situação de risco.&lt;/p&gt;
&lt;h4 id=&quot;balan-ar-o-celular-para-pedir-ajuda-&quot;&gt;Balançar o Celular para Pedir Ajuda:&lt;/h4&gt;
&lt;p&gt;Ative e, ao chacoalhar o celular, ele liga para o 180.&lt;/p&gt;
&lt;h4 id=&quot;escurecimento-por-inclina-o-&quot;&gt;Escurecimento por inclinação:&lt;/h4&gt;
&lt;p&gt;com isso, voce pode controlar o brilho da tela clicando em um botão..., porém, e tipo como se fosse o menor brilho do celular, e ai depois ele deixa a tela preta (nao com brilho e sim colocando a cor), (honestamente, antes aparecia as duas barras, agora elas se escondem!), e o efeito e vitalicio ate fechar e abrir o app!&lt;/p&gt;
&lt;p&gt;&amp;gt;Vale lembrar que ele só funciona dentro da HubActivity!&lt;/p&gt;
&lt;h4 id=&quot;emerg-ncia-&quot;&gt;Emergência:&lt;/h4&gt;
&lt;p&gt;Saindo dessa área, existem botões que abrem o aplicativo nativo do telefone nos números 190, 192 e 180.&lt;/p&gt;
&lt;h4 id=&quot;compartilhamento-r-pido-de-localiza-o-&quot;&gt;Compartilhamento Rápido de Localização:&lt;/h4&gt;
&lt;p&gt;Além disso, existe um botão dentro do aplicativo que obtém a localização atual, monta um link do Google Maps com as coordenadas e abre uma conversa no WhatsApp do 180 com a mensagem preparada. O envio não é automático: é necessário apenas conferir a mensagem e tocar no botão de enviar.&lt;/p&gt;
&lt;h4 id=&quot;contatos-de-confian-a-&quot;&gt;Contatos de Confiança:&lt;/h4&gt;
&lt;p&gt;Além dos contatos de confiança, clicando no primeiro botão você seleciona e salva o contato. O botão abaixo envia um pedido de ajuda para ele.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;e os contatos de confiança, quando são cadrastados eles também são criptografados!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;compartilhamento-de-metadados-&quot;&gt;Compartilhamento de metadados;&lt;/h4&gt;
&lt;p&gt;Ao pressionar o botão de emergência, um botao circular feito com compose aparece no canto inferior direito da tela. O aplicativo obtém a localização atual do dispositivo e abre o menu de compartilhamento do Android.&lt;/p&gt;
&lt;p&gt;A mensagem contém:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Latitude e longitude;&lt;/li&gt;
&lt;li&gt;Link para visualizar a localização no Google Maps;&lt;/li&gt;
&lt;li&gt;Precisão da localização em metros;&lt;/li&gt;
&lt;li&gt;Data e hora;&lt;/li&gt;
&lt;li&gt;Fabricante e modelo do dispositivo;&lt;/li&gt;
&lt;li&gt;Versão do Android;&lt;/li&gt;
&lt;li&gt;Porcentagem da bateria.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A usuária pode escolher por qual aplicativo deseja compartilhar essas informações, como WhatsApp, SMS ou e-mail.&lt;/p&gt;
&lt;p&gt; O compartilhamento não é enviado automaticamente para um contato específico. O usuário precisa escolher o aplicativo e confirmar o envio.&lt;/p&gt;
&lt;p&gt;Vale lembrar que o compose é um overlay usandi a classe &lt;code&gt;EmergencyOverlay&lt;/code&gt;, e a HubActivity chama essa classe!&lt;/p&gt;
&lt;h2 id=&quot;-rea-protegida-&quot;&gt;🔐Área Protegida:&lt;/h2&gt;
&lt;p&gt;Se estiver cadastrado no celular, com Biometric Prompt junto com Device Credential e autenticação weak, pode desbloquear essa área com impressão digital, rosto, PIN, padrão, senha e outros métodos.&lt;/p&gt;
&lt;p&gt;Sistema Cripto (Segurança do App)&lt;/p&gt;
&lt;p&gt;O aplicativo possui um sistema próprio de armazenamento seguro que utiliza criptografia nativa do Android para proteger os dados armazenados.&lt;/p&gt;
&lt;p&gt;Como funciona:&lt;/p&gt;
&lt;p&gt;Quando um dado é salvo no app, ele é criptografado antes de ser armazenado.&lt;/p&gt;
&lt;p&gt;O conteúdo armazenado não permanece em texto simples. Para recuperar o dado, o aplicativo utiliza a chave de criptografia protegida pelo Android e descriptografa o conteúdo somente quando necessário.&lt;/p&gt;
&lt;p&gt;Tecnologias usadas:&lt;/p&gt;
&lt;p&gt;AES-256&lt;br&gt;
Criptografia simétrica utilizada para proteger os dados.&lt;/p&gt;
&lt;p&gt;Android Keystore&lt;br&gt;
Sistema do Android utilizado para proteger as chaves criptográficas.&lt;/p&gt;
&lt;p&gt;Jetpack DataStore Preferences&lt;br&gt;
Sistema utilizado para armazenar as informações persistentes do aplicativo.&lt;/p&gt;
&lt;p&gt;O que cada função faz:&lt;/p&gt;
&lt;p&gt;salvar(chave, valor) → criptografa e armazena o dado&lt;/p&gt;
&lt;p&gt;carregar(chave) → recupera e descriptografa o dado&lt;/p&gt;
&lt;p&gt;remover(chave) → remove um dado específico&lt;/p&gt;
&lt;p&gt;limparTudo() → remove todos os dados armazenados&lt;/p&gt;
&lt;p&gt;Segurança:&lt;/p&gt;
&lt;p&gt;Os dados não são armazenados diretamente em texto simples.&lt;/p&gt;
&lt;p&gt;A criptografia utiliza uma chave protegida pelo Android Keystore, enquanto o armazenamento persistente é realizado pelo Jetpack DataStore.&lt;/p&gt;
&lt;p&gt;Dessa forma, o conteúdo protegido não fica diretamente legível no armazenamento do aparelho.&lt;/p&gt;
&lt;h4 id=&quot;calend-rio-menstrual-&quot;&gt;Calendário Menstrual:&lt;/h4&gt;
&lt;p&gt;Registre como dói cada dia e, com isso, o aplicativo monta um calendário.&lt;/p&gt;
&lt;h4 id=&quot;mapa-&quot;&gt;Mapa:&lt;/h4&gt;
&lt;p&gt;Mostra um mapa da região e, quando a localização estiver disponível e autorizada, permite visualizar a posição atual.&lt;/p&gt;
&lt;h4 id=&quot;raio-seguro&quot;&gt;Raio Seguro&lt;/h4&gt;
&lt;p&gt;Recurso de georreferenciamento que permite definir uma área segura e verificar se a usuária permanece dentro do raio estabelecido.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📌 Define a sua localização como centro da área segura e isso é fixo.&lt;/li&gt;
&lt;li&gt;📏 Permite configurar o raio em metros.&lt;/li&gt;
&lt;li&gt;🗺️ Utiliza OpenStreetMap para visualização do mapa.&lt;/li&gt;
&lt;li&gt;📍 Compara a localização atual com a área definida.&lt;/li&gt;
&lt;li&gt;⚠️ Em caso de saída do raio, exibe um alerta.&lt;/li&gt;
&lt;li&gt;🆘 Disponibiliza a opção Pedir ajuda.&lt;/li&gt;
&lt;li&gt;🔐 Os dados da área segura são armazenados de forma criptografada.&lt;/li&gt;
&lt;li&gt;🚫 Não cria histórico de trajetos no aplicativo.&lt;/li&gt;
&lt;li&gt;🌐 O carregamento dos mapas depende de conexão com a internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;O recurso foi desenvolvido como uma ferramenta de apoio à segurança, permitindo que a própria usuária estabeleça os limites da área que considera segura.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Só funciona dentro da GeoActivity!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;e quando ele detecta que a usuária saiu da área segura, ele mostra um popup com um botão vermelho, e ao clicar ele leva pro telefone nativo do sistema com o 190 já discado!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;di-rio-criptografado-&quot;&gt;Diário criptografado:&lt;/h4&gt;
&lt;p&gt;Usando criptografia, a usuária poderá anotar o que quiser. Com a senha, ficará seguro e também não some, pois estará guardado, e é possivel baixar as páginas desse diário!, e ele tem suporte para markdown...&lt;/p&gt;
&lt;h4 id=&quot;tarefas-&quot;&gt;Tarefas:&lt;/h4&gt;
&lt;p&gt;O sistema permite categorizar tarefas em áreas como estudos, trabalho, pessoal e saúde.&lt;/p&gt;
&lt;p&gt;As tarefas podem ser marcadas como concluídas para acompanhamento do progresso.&lt;/p&gt;
&lt;p&gt;Todas as tarefas são salvas diretamente na classe cripto da usuária.&lt;/p&gt;
&lt;p&gt;Os dados ficam armazenados localmente no dispositivo do usuário.&lt;/p&gt;
&lt;h4 id=&quot;gravador-de-voz-&quot;&gt;Gravador de voz:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Usando uma activity (uma tela) em kotlin, é possivel ter um gravador de voz no app, sendo possível registrar gravações que podem ser utilizadas pela própria usuária para documentação, além do que a usuária quiser, sempre usando permissoes android e com o consentimento da usuária!

GRAVAÇÕES — COMPORTAMENTO E METADADOS

1. DURANTE A GRAVAÇÃO

Ao tocar no botão de gravação:

• O aplicativo inicia a gravação de áudio pelo microfone.
• É criado temporariamente um arquivo .3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho).
• É registrado o horário exato de início da gravação.
• O nível da bateria é registrado.
• O acelerômetro começa a acompanhar os movimentos do aparelho e registra o maior valor de força G observado durante a gravação.
• Se as permissões de localização estiverem disponíveis, o aplicativo acompanha a localização e registra latitude, longitude, precisão e provedor.
• O arquivo original permanece temporário durante o processo.

2. AO ENCERRAR A GRAVAÇÃO

Quando a gravação é encerrada:

• É registrado o horário exato de encerramento.
• É calculado o SHA-256 do áudio original.
• O áudio é criptografado usando AES/GCM.
• O arquivo protegido recebe a extensão .enc.
• É calculado o SHA-256 do arquivo criptografado.
• É criado um arquivo separado .metadata.json contendo os metadados.
• Depois que a criptografia é concluída, o .3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho) original temporário é apagado do armazenamento privado do aplicativo.
• A gravação protegida e seus metadados aparecem na lista do aplicativo.

3. METADADOS GERADOS

TEMPO:

• created_at — timestamp UNIX do início.
• created_at_utc — início em UTC.
• closed_at — timestamp UNIX do encerramento.
• closed_at_utc — encerramento em UTC.
• last_modified — última modificação registrada pelo sistema.
• last_modified_utc — última modificação em UTC.
• ntp_synced — indicação relacionada à configuração de hora automática do Android.

INTEGRIDADE E CRIPTOGRAFIA:

• sha256_raw — SHA-256 do áudio original antes da criptografia.
• sha256_encrypted — SHA-256 do arquivo .enc.
• crypto_algorithm — AES/GCM/NoPadding, 256-bit.
• key_provider — AndroidKeyStore.

LOCALIZAÇÃO:

• gps_latitude — latitude registrada.
• gps_longitude — longitude registrada.
• gps_accuracy — precisão estimada em metros.
• location_provider — provedor utilizado, como GPS ou rede.

DISPOSITIVO:

• device_model — modelo do aparelho.
• device_brand — fabricante/marca.
• android_version — versão do Android.
• api_level — nível da API.
• device_hash — identificador derivado do Android ID e protegido por SHA-256.

SENSORES E AMBIENTE:

• max_g_force — maior aceleração registrada pelo acelerômetro durante a gravação.
• battery_level — nível da bateria no início da gravação.

ARQUIVO:

• file_name — nome do arquivo criptografado.
• file_size_bytes — tamanho do arquivo .enc.
• file_format — formato original do áudio, 3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho)/AMR-NB.
• metadata_version — versão do formato dos metadados.

4. O QUE ACONTECE AO TOCAR EM &quot;DOWNLOAD&quot;

O botão de download NÃO simplesmente copia o .enc.

O aplicativo:

1. Localiza o arquivo .enc protegido.
2. Descriptografa temporariamente o conteúdo.
3. Cria um arquivo .3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho) temporário no cache do aplicativo.
4. Copia esse áudio para a pasta Downloads do Android.
5. No Android 10 ou superior, utiliza o MediaStore.
6. Em versões antigas, utiliza a pasta pública Downloads.
7. Depois da cópia, o arquivo .3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho) temporário utilizado durante o processo é apagado.
8. O arquivo .enc original continua protegido dentro do aplicativo.

5. IMPORTANTE SOBRE O JSON

O .metadata.json é um arquivo separado do áudio.

Por exemplo:

Downloads:

rec_1787821440.3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho)

rec_1787821440.metadata.json

O .3gp, (ou um arquivo 3ga, ou outro arquivo, isso varia conforme o aparelho) é o áudio reproduzível.

O .metadata.json contém as informações técnicas associadas àquela gravação.

6. IMPORTANTE SOBRE A LOCALIZAÇÃO

A localização não é garantida em todas as gravações.

Se a permissão de localização estiver concedida e o Android fornecer uma localização válida, os campos de localização são preenchidos.

Caso contrário, eles ficam como null.

7. IMPORTANTE SOBRE OS METADADOS

Esses metadados são registros técnicos produzidos pelo aplicativo. Eles podem ajudar a documentar como e quando uma gravação foi criada, mas a existência de hashes, localização ou timestamps, por si só, NÃO garante validade jurídica ou prova que um fato ocorreu.

O SHA-256 permite verificar se os bytes de um arquivo correspondem ao conteúdo anteriormente registrado, enquanto os dados de localização, sensores e horário dependem dos recursos e configurações do próprio aparelho.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;meus-arquivos-&quot;&gt;Meus arquivos:&lt;/h4&gt;
&lt;p&gt;Dentro do aplicativo, o Gerenciador de Arquivos do Mulher Amparada funciona como um visualizador. Ele permite navegar pelas pastas e, ao selecionar um arquivo, utiliza o mecanismo do Android para abrir um seletor de aplicativos compatíveis com aquele tipo de arquivo, permitindo que a usuária escolha um aplicativo para visualizá-lo ou executá-lo.&lt;/p&gt;
&lt;p&gt;O aplicativo possui um único botão para alternar entre os diferentes tipos de armazenamento disponíveis, seguindo uma sequência entre:&lt;/p&gt;
&lt;p&gt;Armazenamento interno → cartão SD → dispositivo externo (como pendrive via USB OTG) → armazenamento interno.&lt;/p&gt;
&lt;p&gt;O acesso varia de acordo com o tipo de armazenamento:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Armazenamento interno e cartão SD: utilizam a permissão de Acesso a todos os arquivos, quando concedida pelo Android.&lt;/li&gt;
&lt;li&gt;Dispositivos externos, como pendrives conectados por USB OTG: utilizam o SAF (Storage Access Framework), mecanismo oficial do Android para acesso a documentos e dispositivos de armazenamento externos autorizados pela usuária.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;gt;O Gerenciador de Arquivos (que apenas visualiza) não acessa os arquivos internos do Gravador de Voz do Mulher Amparada nem os dados ou recursos internos de outras funcionalidades do aplicativo. Ele trabalha com arquivos que já estão disponíveis nos armazenamentos do dispositivo e que podem ser acessados pelos mecanismos de armazenamento autorizados pelo Android.&lt;/p&gt;
&lt;p&gt;&amp;gt;Porém, quando eu digo que o Gerenciador de arquivos apenas visualiza, eu quero dizer que ele nao faz operações de arquivos, mas quando clica em um arquivo, ele abre um seletor de apps, ajudando muito para a coleta de evidências!, porém, se a usuária selecionar um app por esse seletor e perder os dados, o desenvolvedor não se responsabiliza!&lt;/p&gt;
&lt;h4 id=&quot;arquivo-seguro-&quot;&gt;Arquivo seguro:&lt;/h4&gt;
&lt;p&gt;Ao tocar neste botão, o app te leva para uma activity que você pode adicionar arquivos usando o storage acess framework (S.A.F) e ele criptografa com a classe cripto, e você pode descriptografar ou excluir!&lt;/p&gt;
&lt;h4 id=&quot;navegador-&quot;&gt;Navegador:&lt;/h4&gt;
&lt;p&gt;A navegação para o Google é feita diretamente pelo código usando window location replace(), sem disponibilizar o endereço como um link na interface. O aplicativo também não implementa um sistema próprio de registro de histórico de navegação, (ou pelo ou menos eu não coloquei na página)&lt;/p&gt;
&lt;p&gt;&amp;gt;O aplicativo possui um navegador interno. A navegação para o Google é feita diretamente pelo código usando link na MainActivity sem disponibilizar o endereço como um link na interface. O aplicativo também não implementa um sistema próprio de registro de histórico de navegação, (ou pelo ou menos eu não coloquei na página)&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;MainActivity &amp;gt; Google &amp;gt; pesquisa/link do Google&lt;/p&gt;
&lt;p&gt;&amp;gt;Voltar &amp;gt; fecha a MainActivity&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;considera-es-finais-&quot;&gt;Considerações finais:&lt;/h3&gt;
&lt;p&gt;e os audios do gravador de voz também são criptografados com a classe Cripto&lt;/p&gt;
&lt;p&gt;e o recurso de proteção ppr barulho, chacalhoar o celular e escurecer a tela, tem como ativar e desativar!&lt;/p&gt;
&lt;p&gt;todas as activitys tem a flag secure!&lt;/p&gt;
&lt;p&gt;e no application do AndroidManifest do app tem allowBackup=&quot;false&quot; &lt;/p&gt;
&lt;h2 id=&quot;-rea-do-amparo-&quot;&gt;❤️🩹Área do amparo:&lt;/h2&gt;
&lt;p&gt;A Área do amparo também utiliza o sistema &quot;BiometricPrompt&quot;, com autenticação por &quot;BIOMETRIC_WEAK&quot; e &quot;DEVICE_CREDENTIAL&quot;.&lt;/p&gt;
&lt;h4 id=&quot;carteirinha-de-reivindica-o-dos-direitos-de-transporte-das-mulheres-&quot;&gt;Carteirinha de reivindicação dos direitos de transporte das mulheres:&lt;/h4&gt;
&lt;p&gt;Após o desbloqueio, é exibida uma carteirinha informativa que não constitui um documento oficial do governo. Ao tocá-la, a usuária é direcionada para uma página destinada à apresentação à equipe de motoristas do transporte, para solicitar o desembarque em um ponto mais seguro durante o período noturno, quando a legislação aplicável permitir.&lt;/p&gt;
&lt;h4 id=&quot;carteirinha-do-gesto-de-ajuda-&quot;&gt;Carteirinha do gesto de ajuda:&lt;/h4&gt;
&lt;p&gt;Também há uma segunda carteirinha que, ao ser selecionada, direciona para um tutorial sobre como realizar o gesto internacional de combate à violência.&lt;/p&gt;
&lt;p&gt;O gesto é um sinal silencioso de pedido de ajuda e pode ser utilizado em diferentes situações de violência. Ele não é exclusivo de mulheres: qualquer pessoa, independentemente de ser homem ou mulher, pode realizá-lo quando precisar sinalizar que necessita de ajuda.&lt;/p&gt;
&lt;p&gt;🌐 &lt;strong&gt;Site oficial:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mulher-amparada.github.io/mulher-amparada-app/&quot;&gt;https://mulher-amparada.github.io/mulher-amparada-app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💻 &lt;strong&gt;Repositório GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mulher-amparada/mulher-amparada-app&quot;&gt;https://github.com/mulher-amparada/mulher-amparada-app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;⚙️Decisões técnicas:&lt;/p&gt;
&lt;p&gt;lembre-se que hoje em dia uso github para compilar os apps e o a16 5g da samsung, então ele nao mata o processo de compilação mais!&lt;/p&gt;
&lt;h4 id=&quot;sobre-o-webview-do-app-&quot;&gt;Sobre o WebView do app:&lt;/h4&gt;
&lt;p&gt;e APENAS A FUNÇÃO DE NAVEGADOR, AS OUTRAS SÃO OU FEITAS COM COMPOSE OU FEITAS COM XML, usa este webview:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;
val settings =  
        webView.settings  

settings.cacheMode =
    android.webkit.WebSettings.LOAD_DEFAULT

settings.loadsImagesAutomatically =
    true

settings.blockNetworkImage =
    false

settings.databaseEnabled =
    true

settings.displayZoomControls =
    false

settings.builtInZoomControls =
    false

settings.setSupportZoom(
    false
)

settings.textZoom =
    100

settings.defaultTextEncodingName =
    &quot;UTF-8&quot;

settings.mixedContentMode =
    android.webkit.WebSettings.MIXED_CONTENT_NEVER_ALLOW

    webView.overScrollMode =  
        View.OVER_SCROLL_NEVER  


    webView.isVerticalScrollBarEnabled =  
        false  


    webView.isFocusable =  
        true  


    webView.isFocusableInTouchMode =  
        true  


    webView.setOnFocusChangeListener {  
            _,  
            _ -&amp;gt; 
    }  


    webView.isHorizontalScrollBarEnabled =  
        false  


    webView.scrollBarStyle =  
        View.SCROLLBARS_INSIDE_OVERLAY  


    settings.javaScriptEnabled =  
        true  


    settings.mediaPlaybackRequiresUserGesture =  
        false  


    settings.domStorageEnabled =  
        true  


    settings.setGeolocationEnabled(  
        true  
    )  


    settings.allowFileAccess =  
        true  


    settings.allowContentAccess =  
        true  


    settings.allowFileAccessFromFileURLs =  
        false  


    settings.allowUniversalAccessFromFileURLs =  
        false  


    settings.javaScriptCanOpenWindowsAutomatically =  
        false  


    settings.setSupportMultipleWindows(  
        false  
    )  

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        val urlAtual = webView.url

        if (urlAtual != null &amp;amp;&amp;amp; urlAtual.contains(&quot;google.com&quot;)) {
            webView.clearHistory()
            finish()
        } else {
            if (webView.canGoBack()) {
                webView.goBack()
            } else {
                isEnabled = false
                onBackPressedDispatcher.onBackPressed()
            }
        }
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;por-que-na-fileactivity-o-bot-o-para-trocar-o-tipo-de-armazenamento-unificado-em-um-s-&quot;&gt;Por que, na FileActivity, o botão para trocar o tipo de armazenamento é unificado em um só?&lt;/h4&gt;
&lt;p&gt;O botão foi unificado para tornar a navegação entre os diferentes tipos de armazenamento mais simples e organizada, especialmente em situações de pânico ou urgência.&lt;/p&gt;
&lt;p&gt;Em vez de apresentar vários botões separados, o mesmo botão alterna sequencialmente entre os armazenamentos disponíveis, seguindo o ciclo 1 → 2 → 3 → 1.&lt;/p&gt;
&lt;p&gt;Essa escolha também foi pensada como uma forma de incentivar uma vistoria sequencial. Ao passar por cada armazenamento antes de retornar ao primeiro, a usuária é estimulada a verificar diferentes locais onde arquivos importantes podem estar, reduzindo a possibilidade de deixar algum armazenamento sem ser conferido por distração ou pressa.&lt;/p&gt;
&lt;p&gt;é tipo assim:&lt;/p&gt;
&lt;p&gt;armazenamento interno, para trocar para cartao sd, clica no botão e troca o icone, mas aí para voltar atrás, tem passar pelo cartao sd e pelo pendrive para só então voltar para o armazenamento interno&lt;/p&gt;
&lt;h4 id=&quot;por-que-o-webview-do-app-n-o-trata-intent-para-abrir-outros-aplicativos-&quot;&gt;Por que o WebView do app não trata &quot;intent&quot; para abrir outros aplicativos?&lt;/h4&gt;
&lt;p&gt;Por questões de segurança e privacidade.&lt;/p&gt;
&lt;p&gt;Permitir que páginas dentro do WebView utilizem &quot;intent&quot; livremente para abrir outros aplicativos ou executar ações externas poderia aumentar os riscos de comportamentos inesperados, abuso de links e rastreamento de usuários.&lt;/p&gt;
&lt;p&gt;Links também podem conter parâmetros de rastreamento, redirecionamentos e outros mecanismos capazes de identificar ou acompanhar a navegação.&lt;/p&gt;
&lt;p&gt;Por isso, o WebView mantém esse comportamento limitado. A exceção é a tela de aplicativos, que possui uma função específica e um fluxo controlado pelo próprio app.&lt;/p&gt;
&lt;h4 id=&quot;sobre-o-tema-escuro-o-conforto-visual-e-a-discri-o-&quot;&gt;Sobre o tema escuro, o conforto visual e a discrição:&lt;/h4&gt;
&lt;p&gt;O aplicativo oferece suporte a ícones monocromáticos, modo claro e modo escuro. O ícone do aplicativo também se adapta automaticamente ao tema selecionado, enquanto os sites acompanham a configuração, alterando seus fundos e elementos visuais conforme o modo ativo.&lt;/p&gt;
&lt;p&gt;E o site do github pages usa essa estrutura (os nomes dos arquivos podem varias):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;claro.css&quot; media=&quot;(prefers-color-scheme: light)&quot;&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;escuro.css&quot; media=&quot;(prefers-color-scheme: dark)&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;-deep-link-do-aplicativo-&quot;&gt;🔗 Deep Link do aplicativo:&lt;/h2&gt;
&lt;p&gt;O aplicativo utiliza um Deep Link personalizado para permitir que o Android abra diretamente o Mulher Amparada:&lt;/p&gt;
&lt;p&gt;com.mulheres://abrir&lt;/p&gt;
&lt;p&gt;Esse endereço utiliza o identificador do aplicativo (&quot;com.mulheres&quot;) como esquema de URI. Ao acessar o link em um dispositivo Android que possui o aplicativo instalado, o sistema pode encaminhar a abertura diretamente para o aplicativo, sem a necessidade de um site ou domínio externo.&lt;/p&gt;
&lt;p&gt;O esquema é registrado no &quot;AndroidManifest.xml&quot; por meio de um &quot;intent-filter&quot;:&lt;/p&gt;
&lt;data&gt;
Assim, o endereço:
com.mulheres://abrir
funciona como uma forma direta de solicitar a abertura do aplicativo Mulher Amparada.
e temos um botão no index.html no site do github pages que leva pro app usando esse link também!
📦Packpage do repositório no github:
ele é um packpage focado para quem quiser divulgar o projeto!
ele contém:
pngs de divulgação com link, texto e qr do site do giithub pages;
o proprio qr do site do giithub pages;
banners do projeto, tanto png, tanto um código para html;
posters de divulgação para o projeto;
icones para sites e para usos no geral, tanto o do mulher amparada tanto o ic_launcher do app, versão normal e monocromático;
o ícone do mulher-amparada-bot
&amp;gt;APENAS PARA AS USUÁRIAS!: Ao indicar o projeto para uma mulher em situação de risco, &lt;strong&gt;priorize o compartilhamento do QR Code impresso ou na tela&lt;/strong&gt;, em vez de enviar links de texto por mensagens (como WhatsApp ou SMS). 
&lt;strong&gt;Por que o QR Code?&lt;/strong&gt; Links de texto deixam rastros fáceis de serem interceptados por agressores que monitoram o celular da vítima. 
&amp;gt;&lt;strong&gt;Como agir:&lt;/strong&gt; Se for imprimir cartazes ou banners, certifique-se de que o QR Code está visível e em alta resolução. Isso permite que a usuária aponte a câmera e acesse o projeto diretamente, minimizando o histórico de digitação e mensagens trocadas.

a pessoa instala pelo github releases!
e quem publica e atualiza essa release é o proprio workflow!
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mulher-amparada/mulher-amparada-app/pkgs/npm/divulgacao&quot;&gt;📦link do packpage!&lt;/a&gt;
🌐 &lt;strong&gt;Site oficial:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mulher-amparada.github.io/mulher-amparada-app/&quot;&gt;https://mulher-amparada.github.io/mulher-amparada-app/&lt;/a&gt;
💻 &lt;strong&gt;Repositório GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mulher-amparada/mulher-amparada-app&quot;&gt;https://github.com/mulher-amparada/mulher-amparada-app&lt;/a&gt;
&lt;/data&gt;</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33225/todas-as-funcoes-do-aplicativo</guid>
<pubDate>Sat, 31 Oct 2026 20:44:09 +0000</pubDate>
</item>
<item>
<title>Why BasicTextField Fails at Scale — And How I Build Kortex</title>
<link>https://coderlegion.com/31745/why-basictextfield-fails-at-scale-and-how-i-buildkortex</link>
<description>&lt;h3 id=&quot;reaching-sub-millisecond-input-latency-on-100k-line-files-using-kotlin-multiplatform-skia-and-virtualized-canvas-layers&quot;&gt;Reaching sub-millisecond input latency on 100K-line files using Kotlin Multiplatform, Skia, and Virtualized canvas layers&lt;/h3&gt;
&lt;p&gt;If you have ever tried building a serious text or code editor in Jetpack Compose or Compose Multiplatform using &lt;code&gt;BasicTextField&lt;/code&gt;, you already know where the ceiling is.&lt;/p&gt;
&lt;p&gt;Declarative UI frameworks excel at forms, chat inputs, and short documents. However, the moment you load a 50,000-line source file, attach real-time syntax highlighting, spawn multiple cursors, and scroll rapidly at 120Hz, the standard composable pipeline starts gasping for air. Recreating &lt;code&gt;AnnotatedString&lt;/code&gt; instances on every keystroke, measuring thousands of nested layout nodes, and allocating objects on the hot rendering path inevitably leads to garbage collection pauses and dropped frames.&lt;/p&gt;
&lt;p&gt;To solve this properly across &lt;strong&gt;Desktop (JVM), Android, iOS, and WebAssembly (Wasm-JS)&lt;/strong&gt;, I built &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/kortex&quot;&gt;Kortex&lt;/a&gt;&lt;/strong&gt; — an open-source, hardware-accelerated code and text editor engine for Kotlin Multiplatform.&lt;/p&gt;
&lt;p&gt;Here is a deep dive into the architecture behind Kortex, why it bypasses standard Compose text components entirely, and how it achieves locked 60+ FPS rendering on massive documents.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-core-philosophy-zero-composable-per-line-overhead&quot;&gt;1. The Core Philosophy: Zero Composable-Per-Line Overhead&lt;/h3&gt;
&lt;p&gt;In a traditional declarative list (like a &lt;code&gt;LazyColumn&lt;/code&gt; of text rows), every visible line is a separate composable node with its own layout pass, modifier chain, and composition state. When you scroll horizontally and vertically simultaneously, or render cross-line selections and diagnostic squiggles, coordinating state across individual row composables becomes an intractable bottleneck.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kortex treats the entire editor viewport as a single hardware-accelerated surface.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of delegating text rendering to Compose UI nodes, Kortex drops down directly to &lt;strong&gt;Skia&lt;/strong&gt; (via Skiko) using a platform-specific &lt;code&gt;SkiaCanvas&lt;/code&gt; abstraction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// JVM, iOS, and WasmJS implementation
@Composable
actual fun SkiaCanvas(
    modifier: Modifier,
    onRender: (canvas: Canvas, width: Int, height: Int) -&amp;gt; Unit
) {
    Box(
        modifier = modifier.drawWithCache {
            val width = size.width.toInt()
            val height = size.height.toInt()

            onDrawBehind {
                if (width &amp;lt;= 0 || height &amp;lt;= 0) return@onDrawBehind

                drawIntoCanvas { canvas -&amp;gt;
                    onRender(canvas.nativeCanvas, width, height)
                }
            }
        }
    )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Android, &lt;code&gt;SkiaCanvas&lt;/code&gt; binds directly to an embedded &lt;code&gt;SkikoSurfaceView&lt;/code&gt; and &lt;code&gt;SkiaLayer&lt;/code&gt;. State changes that require repaints (such as cursor blinking or text input) trigger frame updates via &lt;code&gt;SideEffect { surfaceView?.scheduleFrame() }&lt;/code&gt;, guaranteeing identical 60+ FPS Skia rendering semantics across all four targets without forcing recompositions across the entire Compose UI tree.&lt;/p&gt;
&lt;h4 id=&quot;the-layered-rendering-pipeline&quot;&gt;The Layered Rendering Pipeline&lt;/h4&gt;
&lt;p&gt;Every frame inside &lt;code&gt;EditorCanvas&lt;/code&gt; is composed of lightweight, pre-calculated &lt;code&gt;Layer&lt;/code&gt; primitives rendered in a strict Z-order pass:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Background Canvas&lt;/strong&gt; (&lt;code&gt;BackgroundLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gutter Background&lt;/strong&gt; (&lt;code&gt;gutterBackgroundLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unified Active Line Highlight&lt;/strong&gt; (&lt;code&gt;HighlightedLineLayer&lt;/code&gt; rendered across the full viewport width)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated Gutter Elements&lt;/strong&gt; (&lt;code&gt;GutterLineLayer&lt;/code&gt;, &lt;code&gt;FoldingMarkersLayer&lt;/code&gt;, &lt;code&gt;GutterSeparatorLayer&lt;/code&gt; clipped to &lt;code&gt;gutterWidth&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Viewport Matrix Shift &amp;amp; Clip&lt;/strong&gt; (&lt;code&gt;nativeCanvas.translate(gutterWidth, 0f)&lt;/code&gt; clipped to code width)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Underlay Guides &amp;amp; Markers&lt;/strong&gt; (&lt;code&gt;ColumnRulerLayer&lt;/code&gt;, &lt;code&gt;OccurrencesHighlightLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Selection Regions&lt;/strong&gt; (&lt;code&gt;SelectionLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syntax-Highlighted Text Paragraphs&lt;/strong&gt; (&lt;code&gt;TextContentLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diagnostic Sine-Wave Squiggles&lt;/strong&gt; (&lt;code&gt;DiagnosticsLayer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blinking Multi-Carets&lt;/strong&gt; (&lt;code&gt;CaretLayer&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because the line-number gutter is isolated through its own clip bounds and the code area is shifted with matrix translations, horizontal code scrolling never bleeds into the gutter, and the entire viewport paints in a single unified GPU pass.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-backed-by-krope-logarithmic-text-mutations&quot;&gt;2. Backed by Krope: Logarithmic Text Mutations&lt;/h3&gt;
&lt;p&gt;A fast renderer is useless if typing a character copies a 10MB &lt;code&gt;String&lt;/code&gt; in memory.&lt;/p&gt;
&lt;p&gt;Under the hood, Kortex is powered by &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/krope&quot;&gt;Krope&lt;/a&gt;&lt;/strong&gt;, a thread-safe, persistent &lt;strong&gt;Red-Black Rope&lt;/strong&gt; text buffer built specifically for high-throughput text processing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logarithmic Edits:&lt;/strong&gt; Insertions, deletions, and range replacements execute in $O(\log N)$ time with structural sharing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immutable Snapshots:&lt;/strong&gt; The UI and background analysis pipelines read from an immutable &lt;code&gt;TextSnapshot&lt;/code&gt; tied to a monotonic &lt;code&gt;TextRevision&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delta Position Mapping:&lt;/strong&gt; When a batch edit occurs (for example, typing simultaneously with 15 active cursors), Kortex sorts operations in descending document order so earlier edits never invalidate the offsets of later ones. Once the rope commits the transaction, it returns a &lt;code&gt;TextEdit.Data&lt;/code&gt; delta, and &lt;code&gt;EditorEngine&lt;/code&gt; maps every surviving selection and caret through that delta mathematically:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;private fun mapSingleDelta(position: TextPosition, delta: TextEdit.Data.Single): TextPosition {
    val start = delta.startPosition
    val oldEnd = delta.oldEndPosition
    val newEnd = delta.newEndPosition

    return when {
        position &amp;lt; start -&amp;gt; position
        position in start..oldEnd -&amp;gt; newEnd
        else -&amp;gt; {
            val lineDelta = newEnd.line - oldEnd.line
            when (position.line) {
                oldEnd.line -&amp;gt; {
                    val colDelta = newEnd.column - oldEnd.column
                    TextPosition(line = position.line + lineDelta, column = position.column + colDelta)
                }
                else -&amp;gt; TextPosition(line = position.line + lineDelta, column = position.column)
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For structural line operations like &lt;code&gt;MoveLineUp&lt;/code&gt; and &lt;code&gt;MoveLineDown&lt;/code&gt; (&lt;code&gt;Alt + Up/Down&lt;/code&gt;), calculating position via delta projection alone would snap the caret to the edge of the replaced range. Kortex resolves this by supporting explicit &lt;strong&gt;&lt;code&gt;targetSelections&lt;/code&gt;&lt;/strong&gt; inside &lt;code&gt;EditorCommand.ApplyText&lt;/code&gt;, enabling line swaps to carry their carets and selections to adjacent lines without caret drift.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-killing-gc-pauses-with-bounded-native-lru-caches&quot;&gt;3. Killing GC Pauses with Bounded Native LRU Caches&lt;/h3&gt;
&lt;p&gt;Skia objects like &lt;code&gt;Paragraph&lt;/code&gt;, &lt;code&gt;TextLine&lt;/code&gt;, and &lt;code&gt;Paint&lt;/code&gt; wrap native C++ resources. Allocating and destroying them on every 16ms frame (or 8ms on 120Hz ProMotion displays) quickly overwhelms the memory allocator.&lt;/p&gt;
&lt;p&gt;Kortex solves this with a custom thread-safe &lt;code&gt;LruCache&lt;/code&gt; extending &lt;code&gt;CloseableResource&lt;/code&gt;. Whenever an item is evicted from the cache—or when the editor composable leaves the composition tree—its underlying native Skia pointer is deterministically closed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;internal class CodeParagraphCache(capacity: Int = 1000) : LruCache&amp;lt;CodeParagraphCache.Key, Paragraph&amp;gt;(capacity) {
    data class Key(
        val line: Int,
        val text: String,
        val tokens: List&amp;lt;Token&amp;gt;,
        val font: EditorFont,
        val syntaxTheme: SyntaxTheme,
    )

    override val factory: Key.() -&amp;gt; Paragraph = {
        // Builds and lays out the Skia Paragraph only on a cache miss
        ...
        builder.build().apply { layout(Float.MAX_VALUE) }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Crucially, this zero-allocation philosophy extends to input gestures. Dragging the mouse to select text calls &lt;code&gt;getOffsetAtCoordinate&lt;/code&gt; on every pixel change. Re-parsing a Skia paragraph on each pointer move would create unacceptable jank. In Kortex, &lt;code&gt;EditorFont&lt;/code&gt; manages an internal &lt;code&gt;measurementParagraphCache&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;private val measurementParagraphCache = object : LruCache&amp;lt;String, Paragraph&amp;gt;(capacity = 128) {
    override val factory: String.() -&amp;gt; Paragraph = {
        val textToMeasure = this
        TextStyle().apply {
            this.fontFamilies = arrayOf(*Emails are not allowed*lyName)
            this.fontSize = *Emails are not allowed*
        }.use { baseTextStyle -&amp;gt;
            ParagraphStyle().apply {
                this.maxLinesCount = 1
                this.textStyle = baseTextStyle
            }.use { style -&amp;gt;
                buildParagraph(style) {
                    pushStyle(baseTextStyle)
                    addText(textToMeasure)
                    popStyle()
                }.use { builder -&amp;gt;
                    builder.build().apply { layout(Float.MAX_VALUE) }
                }
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During continuous 120 FPS scrolling, cursor blinking, or rapid mouse selection, Kortex hits its bounded caches with near-100% hit rates, resulting in &lt;strong&gt;zero native allocations on the render loop&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-viewport-virtualization-amp-logarithmic-code-folding-foldmap-&quot;&gt;4. Viewport Virtualization &amp;amp; Logarithmic Code Folding (&lt;code&gt;FoldMap&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;How do you render a 200,000-line file when 50 different functions and classes are collapsed?&lt;/p&gt;
&lt;p&gt;Iterating through lines to check which ones are hidden would turn rendering into an $O(N)$ operation. Instead, Kortex compiles collapsed regions into an immutable &lt;strong&gt;&lt;code&gt;FoldMap&lt;/code&gt;&lt;/strong&gt;—a sorted, merged interval table storing cumulative hidden line counts and visual thresholds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;internal data class Interval(
    val startLine: Int,
    val endLine: Int,
    val hiddenCount: Int,
    val cumulativeHiddenBefore: Int,
    val visualStartThreshold: Int,
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Converting between a &lt;strong&gt;Visual Line&lt;/strong&gt; (what the user sees on screen) and a &lt;strong&gt;Document Line&lt;/strong&gt; (the actual index in the Rope buffer) is a binary search over active intervals running in $O(\log K)$ time, where $K$ is the number of folded blocks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Viewport.of(...)&lt;/code&gt; calculates the exact visible visual line slice (&lt;code&gt;startVisualLine..endVisualLine&lt;/code&gt;) from &lt;code&gt;scrollY&lt;/code&gt; and &lt;code&gt;lineHeight&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each visible line is mapped to its true document line via &lt;code&gt;foldMap.visualToDocumentLine(visualLineIndex)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Collapsed lines are completely culled before Skia ever touches them.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-pure-unidirectional-mvi-amp-smart-undo-redo-coalescing&quot;&gt;5. Pure Unidirectional MVI &amp;amp; Smart Undo/Redo Coalescing&lt;/h3&gt;
&lt;p&gt;UI components in Kortex never mutate state directly. The entire &lt;code&gt;:kortex-core&lt;/code&gt; module is completely headless (zero Compose dependencies) and operates as a deterministic state machine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;(EditorState, EditorAction) -&amp;gt; EditorResult(newState, EditorCommand?)

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure Reducers:&lt;/strong&gt; &lt;code&gt;CaretReducer&lt;/code&gt;, &lt;code&gt;SelectionReducer&lt;/code&gt;, &lt;code&gt;EditReducer&lt;/code&gt;, and &lt;code&gt;ViewportReducer&lt;/code&gt; compute next states purely in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Cursor Normalization:&lt;/strong&gt; Every time cursors move or expand, &lt;code&gt;Selection.normalize(...)&lt;/code&gt; sorts all active selections and automatically merges overlapping or touching ranges while preserving selection direction (&lt;code&gt;FORWARD&lt;/code&gt; vs. &lt;code&gt;BACKWARD&lt;/code&gt;), surrogate pairs (emoji), and &lt;code&gt;stickyColumn&lt;/code&gt; offsets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sliding-Window Undo/Redo:&lt;/strong&gt; &lt;code&gt;HistoryManager&lt;/code&gt; records forward and inverse &lt;code&gt;TextOperation.Data&lt;/code&gt; commands and automatically merges contiguous single-line insertions or deletions if they occur within a &lt;strong&gt;700ms&lt;/strong&gt; window—restoring exact multi-cursor positions when undone.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-full-ide-tooling-async-analysis-squiggles-amp-hierarchical-themes&quot;&gt;6. Full IDE Tooling: Async Analysis, Squiggles &amp;amp; Hierarchical Themes&lt;/h3&gt;
&lt;p&gt;On top of the plain-text &lt;code&gt;:kortex-core&lt;/code&gt; engine sits &lt;code&gt;:kortex-code&lt;/code&gt; and &lt;code&gt;:kortex-code-compose&lt;/code&gt;, turning the editor into a complete IDE component.&lt;/p&gt;
&lt;h4 id=&quot;debounced-coroutine-pipelines&quot;&gt;Debounced Coroutine Pipelines&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;CodeEditorEngine&lt;/code&gt; binds to a pluggable &lt;code&gt;LanguageAnalysisService&lt;/code&gt; using Kotlin Coroutine Flows on &lt;code&gt;Dispatchers.Default&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tokenization (&lt;code&gt;TokenProvider&lt;/code&gt;):&lt;/strong&gt; Debounced at &lt;strong&gt;50ms&lt;/strong&gt;, dropping stale snapshots via &lt;code&gt;distinctUntilChangedBy(TextSnapshot::revision)&lt;/code&gt; and &lt;code&gt;mapLatest&lt;/code&gt;. Existing folding regions and symbol occurrences are preserved during token updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diagnostics (&lt;code&gt;DiagnosticsProvider&lt;/code&gt;):&lt;/strong&gt; Debounced at &lt;strong&gt;300ms&lt;/strong&gt;, feeding &lt;code&gt;DiagnosticsLayer&lt;/code&gt; to draw procedural sine-wave/zigzag paths directly under erroneous tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hover Documentation (&lt;code&gt;DocumentationProvider&lt;/code&gt;):&lt;/strong&gt; Debounced at &lt;strong&gt;250ms&lt;/strong&gt;, resolving documentation for the symbol under the primary caret and updating &lt;code&gt;CodeEditorState.documentation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IntelliSense (&lt;code&gt;CompletionProvider&lt;/code&gt;):&lt;/strong&gt; Floating popup anchored to the active caret's visual coordinates with keyboard navigation (&lt;code&gt;Up&lt;/code&gt;, &lt;code&gt;Down&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;/&lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Escape&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;hierarchical-token-theming&quot;&gt;Hierarchical Token Theming&lt;/h4&gt;
&lt;p&gt;Inspired by TextMate and VS Code scopes, &lt;code&gt;SyntaxTheme&lt;/code&gt; resolves token colors hierarchically using dot-separated fallbacks. If your lexer emits &lt;code&gt;keyword.control&lt;/code&gt; and the active theme only defines a color for &lt;code&gt;keyword&lt;/code&gt;, Kortex walks up the scope tree automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val syntaxTheme = SyntaxTheme.build(defaultText = Color(0xFFABB2BF)) {
    put(TokenType.KEYWORD.name, Color(0xFFC678DD))
    put(TokenType.KEYWORD_CONTROL.name, Color(0xFFE06C75)) // Overrides &quot;keyword&quot;
    put(TokenType.FUNCTION.name, Color(0xFF61AFEF))
    put(TokenType.STRING.name, Color(0xFF98C379))
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;getting-started-in-30-seconds&quot;&gt;Getting Started in 30 Seconds&lt;/h3&gt;
&lt;p&gt;Kortex is published on &lt;strong&gt;Maven Central&lt;/strong&gt; and divided into four modular artifacts so you only pull what you need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies {
    implementation(&quot;io.github.numq.kortex:core:1.1.0&quot;)         // Headless engine
    implementation(&quot;io.github.numq.kortex:core-compose:1.1.0&quot;) // Plain text Skia editor
    implementation(&quot;io.github.numq.kortex:code:1.1.0&quot;)         // Code analysis engine
    implementation(&quot;io.github.numq.kortex:code-compose:1.1.0&quot;) // Full IDE Composable
    implementation(&quot;io.github.numq.krope:text:1.1.1&quot;)          // Rope text buffer
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Embedding a full-featured code editor in your Compose Multiplatform app takes just a few lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun IdeEditorScreen() {
    val scope = rememberCoroutineScope()

    val buffer = remember {
        RopeTextBuffer(
            initialText = &quot;fun main() {\n    println(\&quot;Hello, Kortex!\&quot;)\n}&quot;,
            initialLineEnding = TextLineEnding.LF,
            initialEncoding = Encoding.UTF8,
            enablePooling = true
        )
    }

    val editorEngine = remember(buffer) { EditorEngine(scope = scope, buffer = buffer) }
    val codeEngine = remember(editorEngine) {
        CodeEditorEngine(
            scope = scope,
            engine = editorEngine,
            analysisService = myLanguageAnalysisService
        )
    }

    val font = remember {
        val typeface = FontMgr.default.matchFamilyStyle(&quot;JetBrains Mono&quot;, FontStyle.NORMAL)
            ?: Typeface.makeEmpty()
        EditorFont(typeface = typeface, size = 14f, lineSpacing = 1.25f)
    }

    DisposableEffect(font) {
        onDispose {
            font.close()
        }
    }

    val theme = rememberCodeEditorTheme()

    KortexCodeEditor(
        engine = codeEngine,
        font = font,
        theme = theme,
        columnRuler = 80,
        modifier = Modifier.fillMaxSize()
    )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Out of the box, you get &lt;code&gt;Alt + Click&lt;/code&gt; multi-cursors, &lt;code&gt;Ctrl/Cmd + D&lt;/code&gt; next-occurrence selection, code folding, smart indentation, line moving (&lt;code&gt;Alt + Up/Down&lt;/code&gt;), comment toggling (&lt;code&gt;Ctrl/Cmd + /&lt;/code&gt;), right-click context menus, and native desktop scrollbar adapters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;explore-the-source-code&quot;&gt;Explore the Source Code&lt;/h3&gt;
&lt;p&gt;Both &lt;strong&gt;Kortex&lt;/strong&gt; and &lt;strong&gt;Krope&lt;/strong&gt; are 100% open-source under the Apache 2.0 License. Whether you are building a developer tool, a Markdown note-taking app, a database query console, or a full multiplatform IDE, I would love for you to try it out and share your feedback!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kortex:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/kortex&quot;&gt;github.com/numq/kortex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Krope:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/krope&quot;&gt;github.com/numq/krope&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31745/why-basictextfield-fails-at-scale-and-how-i-buildkortex</guid>
<pubDate>Mon, 19 Oct 2026 14:55:34 +0000</pubDate>
</item>
<item>
<title>Kempo: Real-Time Audio Time-Stretching, Pitch-Shifting, and Formant Control in Pure KMP</title>
<link>https://coderlegion.com/31021/kempo-real-time-audio-time-stretching-pitch-shifting-and-formant-control-in-pure-kmp</link>
<description>&lt;h4 id=&quot;zero-allocation-streaming-phase-locked-clarity-and-multichannel-spatial-coherence-across-jvm-android-ios-and-webassembly-&quot;&gt;Zero-allocation streaming, phase-locked clarity, and multichannel spatial coherence across JVM, Android, iOS, and WebAssembly.&lt;/h4&gt;
&lt;p&gt;Whether you are building a podcast player with variable playback speed, a music practice app that slows down a complex guitar solo without dropping the tuning, a real-time voice modulator, or a spatial audio engine for a game, you eventually hit the same fundamental challenge: &lt;strong&gt;decoupling time from pitch&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In raw digital audio, playback speed and pitch are locked together. Play a buffer twice as fast, and the pitch jumps up a full octave. Slow it down, and everything drags into a muddy baritone.&lt;/p&gt;
&lt;p&gt;Breaking that link—while keeping drums punchy, vocals natural, and the stereo image intact—requires a dedicated Digital Signal Processing (DSP) engine.&lt;/p&gt;
&lt;p&gt;Meet &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/kempo&quot;&gt;Kempo&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;io.github.numq:kempo&lt;/code&gt;): an open-source, pure Kotlin Multiplatform audio time-stretching and pitch-shifting library built for studio-grade acoustic transparency and sub-millisecond real-time streaming across &lt;strong&gt;JVM, Android, iOS, and WebAssembly (Wasm/JS)&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-kempo-brings-to-your-audio-stack&quot;&gt;What Kempo Brings to Your Audio Stack&lt;/h3&gt;
&lt;p&gt;Kempo is designed to be drop-in simple for high-level application code while exposing full, granular control for low-level audio threads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Independent Time, Pitch, and Formant Control:&lt;/strong&gt; Speed up or slow down audio (&lt;code&gt;0.25x&lt;/code&gt; to &lt;code&gt;3.0x&lt;/code&gt; and beyond), transpose musical pitch in semitones, and shift vocal timbre independently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero-Allocation Real-Time Streaming:&lt;/strong&gt; Once configured, the block-by-block processing loop allocates &lt;strong&gt;0 bytes on the heap&lt;/strong&gt;, eliminating Garbage Collection pauses on high-priority audio threads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vertical Harmonic Phase-Locking:&lt;/strong&gt; Keeps transients crisp and eliminates the hollow, &quot;underwater&quot; phasing artifacts common in basic time-stretchers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multichannel &amp;amp; 5.1 Surround Coherence:&lt;/strong&gt; Preserves exact inter-channel phase relationships across stereo and surround tracks so the soundstage never collapses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sample-Accurate Offline Rendering:&lt;/strong&gt; Built-in pre-roll warm-up, algorithmic latency compensation, and automatic overlap-add tail flushing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-In PCM-16 Serialization:&lt;/strong&gt; Convert raw little-endian or big-endian WAV/PCM byte arrays directly into normalized floating-point tracks and back.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-kempo-keeps-audio-crisp-without-the-artifacts-&quot;&gt;How Kempo Keeps Audio Crisp (Without the Artifacts)&lt;/h3&gt;
&lt;p&gt;If you have ever used a naive time-stretching algorithm, you probably recognize three classic audio defects: smeared drum hits, &quot;chipmunk&quot; vocals, and a collapsing stereo field. Kempo solves all three at the engine level.&lt;/p&gt;
&lt;h4 id=&quot;1-eliminating-transient-smearing-with-vertical-phase-locking&quot;&gt;1. Eliminating Transient Smearing with Vertical Phase-Locking&lt;/h4&gt;
&lt;p&gt;Standard phase vocoders split audio into hundreds of narrow frequency bins and advance the phase of each bin over time. Because each bin is updated in isolation, the natural harmonic alignment between a fundamental note and its overtones quickly drifts apart. Percussive attacks lose their snap, and instruments sound phasey and diffuse.&lt;/p&gt;
&lt;p&gt;Kempo continuously analyzes the smoothed spectral energy contour of each frame to locate prominent spectral peaks. Instead of letting neighboring bins drift, Kempo performs &lt;strong&gt;vertical phase-locking&lt;/strong&gt;—anchoring the phase trajectories of surrounding frequency bins to their dominant harmonic peaks. Harmonics move together as a unified acoustic event, keeping transients tight.&lt;/p&gt;
&lt;p&gt;When stretching audio to extreme lengths (beyond &lt;code&gt;2.0x&lt;/code&gt;), Kempo automatically transitions into controlled stochastic phase dispersion, preventing the metallic ringing that usually plagues ultra-slow playback.&lt;/p&gt;
&lt;h4 id=&quot;2-fixing-the-chipmunk-effect-with-formant-decoupling&quot;&gt;2. Fixing the &quot;Chipmunk&quot; Effect with Formant Decoupling&lt;/h4&gt;
&lt;p&gt;When you pitch a human voice up by +5 semitones, you aren't just raising the pitch of the vocal cords—you are also shrinking the perceived size of the singer's vocal tract. The result is the notorious &quot;chipmunk&quot; effect (or the &quot;monster&quot; effect when pitching down).&lt;/p&gt;
&lt;p&gt;In Kempo, pitch and vocal timbre (formants) are decoupled. Kempo extracts the spectral envelope of the signal and allows you to shift or lock formants independently of the musical transposition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Natural Pitch Shifting:&lt;/strong&gt; Enable formant compensation (&lt;code&gt;compensatePitch = true&lt;/code&gt;) to keep a singer's vocal tract resonance anchored while changing their melody.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creative Voice Design:&lt;/strong&gt; Shift formants up or down while keeping pitch at &lt;code&gt;0 st&lt;/code&gt; to transform the character and age of a voice without changing the note being sung.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-preserving-stereo-width-and-5-1-surround-imaging&quot;&gt;3. Preserving Stereo Width and 5.1 Surround Imaging&lt;/h4&gt;
&lt;p&gt;Our ears localize sound in space by detecting tiny timing and phase differences between the left and right channels. If an audio processor stretches the Left and Right channels independently, their relative phases drift apart—causing the stereo image to wobble and center-panned vocals to hollow out when downmixed.&lt;/p&gt;
&lt;p&gt;Kempo processes multichannel audio coherently. At every frequency band, it identifies the dominant channel and locks the Inter-channel Phase Differences (IPD) of all remaining channels to that reference. Whether you are processing a stereo master or a discrete 6-channel 5.1 surround mix, panning stays locked and center dialogue remains phase-aligned.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;Kempo is published on Maven Central. Add the dependency to your &lt;code&gt;build.gradle.kts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(&quot;io.github.numq:kempo:1.0.0&quot;)
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or for a single-platform JVM/Android module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies {
    implementation(&quot;io.github.numq:kempo:1.0.0&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1-the-one-liner-offline-track-processing&quot;&gt;1. The One-Liner: Offline Track Processing&lt;/h4&gt;
&lt;p&gt;If you have raw 16-bit PCM bytes (from a WAV file, network stream, or microphone recording) and want to stretch or transpose them in a background coroutine, &lt;code&gt;PcmConverter&lt;/code&gt; and &lt;code&gt;AudioTrack&lt;/code&gt; handle the entire pipeline in a few lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;import io.github.numq.kempo.PcmConverter

// 1. Decode interleaved 16-bit PCM bytes into a normalized [-1.0f, 1.0f] AudioTrack
val track = PcmConverter.fromPcm16(
    bytes = rawWavBytes,
    channels = 2,
    sampleRate = 44100f,
    isLittleEndian = true
)

// 2. Speed up by 25%, pitch up by 3 semitones, keep natural formants
val stretchedTrack = track.stretch(
    timeRatio = 1.25f,
    pitchSemitones = 3.0f,
    formantSemitones = 0.0f
)

// 3. Convert back to 16-bit PCM bytes for playback or file export
val outputBytes = PcmConverter.toPcm16(stretchedTrack, isLittleEndian = true)

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-zero-allocation-real-time-streaming&quot;&gt;2. Zero-Allocation Real-Time Streaming&lt;/h4&gt;
&lt;p&gt;Real-time audio callbacks operate under strict deadlines. Allocating arrays or objects inside the render loop will eventually trigger the garbage collector and cause audible clicks.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KempoStretch&lt;/code&gt; pre-allocates all ring buffers, FFT plans, twiddle factors, and spectral workspaces upfront during &lt;code&gt;presetDefault()&lt;/code&gt; or &lt;code&gt;configure()&lt;/code&gt;. Inside your audio callback, &lt;code&gt;process()&lt;/code&gt; mutates pre-allocated buffers in place:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;import io.github.numq.kempo.KempoStretch
import kotlin.math.max

class LiveAudioProcessor(
    channels: Int = 2,
    sampleRate: Float = 48000f
) {
    private val stretch = KempoStretch().apply {
        presetDefault(channels, sampleRate)
    }

    val outBlockSize: Int = stretch.defaultInterval
    private val outBuffer = Array(channels) { FloatArray(outBlockSize) }

    fun updateParameters(pitchSemitones: Float, formantSemitones: Float) {
        stretch.setTransposeSemitones(pitchSemitones)
        stretch.setFormantSemitones(formantSemitones, compensatePitch = true)
    }

    // Safe for high-priority audio threads: 0 B heap allocations
    fun processFrame(
        inputChannels: Array&amp;lt;FloatArray&amp;gt;,
        playbackSpeed: Float
    ): Array&amp;lt;FloatArray&amp;gt; {
        val inSamples = max(1, (outBlockSize * playbackSpeed).toInt())
        stretch.process(
            inputs = inputChannels,
            inputSamples = inSamples,
            outputs = outBuffer,
            outputSamples = outBlockSize
        )
        return outBuffer
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Need to support interactive scrubbing on a timeline without clicks or phase resets? Use &lt;code&gt;stretch.seek()&lt;/code&gt; before resuming playback to pre-seed the internal phase accumulators with the audio frames right before the new cursor position:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val seekPreRoll = minOf(stretch.seekLength(), targetSampleIndex)
stretch.seek(
    inputs = track.channels,
    inputSamples = seekPreRoll,
    playbackRate = 1.0,
    inputOffset = (targetSampleIndex - seekPreRoll).coerceAtLeast(0)
)

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-sample-accurate-offline-rendering-with-exact-&quot;&gt;3. Sample-Accurate Offline Rendering with &lt;code&gt;exact()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;When rendering an audio clip for a DAW timeline or a game asset pipeline, you often need the output buffer to match an exact target sample count—without leading silence from the STFT window latency or cut-off reverb tails at the end.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KempoStretch.exact()&lt;/code&gt; automates the entire offline lifecycle: it calculates the required pre-roll (&lt;code&gt;outputSeekLength&lt;/code&gt;), warms up the vocoder state (&lt;code&gt;outputSeek&lt;/code&gt;), processes the body of the track, and flushes the final synthesis tails (&lt;code&gt;flush&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;import io.github.numq.kempo.KempoStretch
import kotlin.math.roundToInt

fun renderExactClip(
    input: Array&amp;lt;FloatArray&amp;gt;,
    sampleRate: Float,
    timeRatio: Float,
    pitchSemitones: Float
): Array&amp;lt;FloatArray&amp;gt; {
    val channels = input.size
    val inSamples = input[0].size
    val outSamples = (inSamples / timeRatio).roundToInt().coerceAtLeast(1)
    val output = Array(channels) { FloatArray(outSamples) }

    val stretch = KempoStretch().apply {
        presetDefault(channels, sampleRate)
        setTransposeSemitones(pitchSemitones)
    }

    stretch.exact(
        inputs = input,
        inputSamples = inSamples,
        outputs = output,
        outputSamples = outSamples
    )
    return output
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-custom-frequency-mapping-amp-tonality-limits&quot;&gt;4. Custom Frequency Mapping &amp;amp; Tonality Limits&lt;/h4&gt;
&lt;p&gt;For experimental sound design, spectral effects, or specialized speech conditioning, Kempo lets you bypass standard semitone math entirely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tonality Limits:&lt;/strong&gt; When transposing speech or vocals up, multiplying high-frequency sibilance (&quot;s&quot; and &quot;sh&quot; sounds) can make them sound lispy or artificial. Passing a &lt;code&gt;tonalityLimit&lt;/code&gt; to &lt;code&gt;setTransposeSemitones(semitones, tonalityLimit)&lt;/code&gt; transitions the upper spectrum from multiplicative harmonic scaling to a clean linear shift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Transfer Functions:&lt;/strong&gt; Pass any lambda to &lt;code&gt;setFreqMap&lt;/code&gt; to warp normalized frequencies &lt;code&gt;[0.0f, 0.5f]&lt;/code&gt; non-linearly:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;stretch.setFreqMap { normalizedFreq -&amp;gt;
    // Custom non-linear spectral warping
    (normalizedFreq * 1.15f).coerceIn(0f, 0.5f)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineered-for-speed-in-pure-kotlin&quot;&gt;Engineered for Speed in Pure Kotlin&lt;/h3&gt;
&lt;p&gt;Writing high-throughput DSP in pure Kotlin without falling back to platform-specific C/C++ libraries requires careful attention to memory layout and CPU cache locality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flat Power-of-Two Ring Buffers (&lt;code&gt;MultiChannelBuffer&lt;/code&gt;):&lt;/strong&gt; Instead of nested object arrays or modulo arithmetic, Kempo backs all channels with a single flat &lt;code&gt;FloatArray&lt;/code&gt;. Wrap-around indices are resolved via bitwise masks (&lt;code&gt;(head + offset) and mask&lt;/code&gt;), and overlap-add accumulation uses 4x unrolled loops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In-Place Mixed-Radix FFT (&lt;code&gt;FastFft&lt;/code&gt; &amp;amp; &lt;code&gt;ModifiedRealFFT&lt;/code&gt;):&lt;/strong&gt; Supports radix-2, radix-3, radix-4, and composite window sizes with precomputed twiddle tables. Real signals are evaluated on a half-bin shifted grid &lt;code&gt;(k + 0.5) / N&lt;/code&gt;, avoiding DC and Nyquist edge singularities and cutting transform overhead in half.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split Computation Mode (&lt;code&gt;split = true&lt;/code&gt;):&lt;/strong&gt; In real-time audio, doing a full STFT analysis and synthesis every &lt;code&gt;N&lt;/code&gt; samples can cause periodic CPU spikes. Enabling &lt;code&gt;split = true&lt;/code&gt; in &lt;code&gt;configure()&lt;/code&gt; or &lt;code&gt;presetCheaper()&lt;/code&gt; slices the spectral workload into small incremental steps distributed evenly across every sample frame.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Silence Bypass:&lt;/strong&gt; When input energy drops below the noise floor, Kempo automatically bypasses heavy spectral math while keeping ring buffers synchronized.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In JMH benchmarks on standard desktop JVM hardware (44.1 kHz audio), Kempo processes a real-time streaming block in &lt;strong&gt;~220 μs (mono)&lt;/strong&gt; and &lt;strong&gt;~484 μs (stereo)&lt;/strong&gt;, and renders offline audio at &lt;strong&gt;over 128x real-time speed&lt;/strong&gt; for mono and &lt;strong&gt;~70x real-time&lt;/strong&gt; for stereo.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-live-the-compose-desktop-studio-app&quot;&gt;Try It Live: The Compose Desktop Studio App&lt;/h3&gt;
&lt;p&gt;Want to hear how Kempo handles your own audio files before writing a line of code?&lt;/p&gt;
&lt;p&gt;The repository includes an interactive &lt;strong&gt;Compose Multiplatform Desktop Studio&lt;/strong&gt; application (&lt;code&gt;:example&lt;/code&gt;). Drop any &lt;code&gt;.wav&lt;/code&gt; file onto the window to inspect its waveform, scrub through the timeline in real time, and tweak live parameters on the fly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Playback Speed (&lt;code&gt;0.25x&lt;/code&gt; – &lt;code&gt;3.00x&lt;/code&gt;)&lt;/strong&gt; with instant presets (&lt;code&gt;0.50x&lt;/code&gt;, &lt;code&gt;0.75x&lt;/code&gt;, &lt;code&gt;1.25x&lt;/code&gt;, &lt;code&gt;1.50x&lt;/code&gt;, &lt;code&gt;2.00x&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pitch Shift (&lt;code&gt;-12 st&lt;/code&gt; to &lt;code&gt;+12 st&lt;/code&gt;)&lt;/strong&gt; in semitone steps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Formant Shift (&lt;code&gt;-12 st&lt;/code&gt; to &lt;code&gt;+12 st&lt;/code&gt;)&lt;/strong&gt; for real-time vocal timbre shaping&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;explore-the-code&quot;&gt;Explore the Code&lt;/h3&gt;
&lt;p&gt;Check out the full source code, architecture docs, and the runnable CLI attack simulation on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/kempo&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31021/kempo-real-time-audio-time-stretching-pitch-shifting-and-formant-control-in-pure-kmp</guid>
<pubDate>Wed, 14 Oct 2026 07:01:23 +0000</pubDate>
</item>
<item>
<title>Why HTTPS Won’t Save Your Checkout Flow: Building Zero-Trust State Machines in Kotlin Multiplatform</title>
<link>https://coderlegion.com/31019/https-wont-save-your-checkout-flow-building-zero-trust-state-machines-kotlin-multiplatform</link>
<description>&lt;h4 id=&quot;how-to-protect-multi-step-workflows-fintech-funnels-and-server-driven-ui-from-step-skipping-state-tampering-and-replay-attacks-with-zero-runtime-dependencies-&quot;&gt;How to protect multi-step workflows, fintech funnels, and Server-Driven UI from step-skipping, state tampering, and replay attacks — with zero runtime dependencies.&lt;/h4&gt;
&lt;p&gt;Imagine a standard e-commerce checkout flow: &lt;strong&gt;Shopping Cart → Shipping Address → Promo Code → Payment Authorization&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You use HTTPS. You validate JWT tokens on every request. Your backend endpoints are covered with unit tests. You feel safe.&lt;/p&gt;
&lt;p&gt;Then someone opens your app on a rooted device, hooks into memory with Frida, or simply manipulates an internal deep link. Instead of going from &lt;strong&gt;Cart&lt;/strong&gt; to &lt;strong&gt;Shipping&lt;/strong&gt; (where a $50 express delivery fee is calculated), the client dispatches a transition straight from &lt;strong&gt;Cart&lt;/strong&gt; to &lt;strong&gt;Payment&lt;/strong&gt;. Or worse, in a Server-Driven UI (SDUI) flow, an attacker replays a cached JSON payload from five minutes ago where a KYC verification flag was temporarily set to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTPS only encrypts the pipe. JWTs only verify who is holding the token.&lt;/strong&gt; Neither guarantees that your client-side state machine actually followed the legal sequence of steps to reach its current screen.&lt;/p&gt;
&lt;p&gt;To fix this at the architectural level without littering business logic with endless &lt;code&gt;if-else&lt;/code&gt; checks, I built &lt;strong&gt;TrustChain&lt;/strong&gt; — a pure functional, cryptographically verifiable state machine ledger for &lt;strong&gt;Kotlin Multiplatform (KMP)&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mental-model-think-git-commits-for-your-app-state&quot;&gt;The Mental Model: Think &quot;Git Commits&quot; for Your App State&lt;/h3&gt;
&lt;p&gt;You don’t need a PhD in cryptography or heavy blockchain consensus algorithms to secure a user journey. You just need &lt;strong&gt;strict lineage&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think about how Git works: every commit contains a hash of its own content plus the exact hash of the parent commit. If you try to alter history or insert a commit out of nowhere, the hashes don't match, and Git rejects the tree immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TrustChain brings this exact mechanism to your application’s navigation and state stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every screen or workflow step has a unique &lt;code&gt;hash&lt;/code&gt; representing its current state, and a &lt;code&gt;previousHash&lt;/code&gt; pointing to the exact step that preceded it.&lt;/li&gt;
&lt;li&gt;When the user moves forward, the incoming step’s &lt;code&gt;previousHash&lt;/code&gt; &lt;strong&gt;must&lt;/strong&gt; match the &lt;code&gt;hash&lt;/code&gt; of the currently active step on top of the stack.&lt;/li&gt;
&lt;li&gt;Partial state updates (like applying a promo code or updating live order status over WebSockets) arrive as &lt;strong&gt;signed deltas&lt;/strong&gt; (&lt;code&gt;DeltaBlock&lt;/code&gt;). They verify the current hash, check the server’s asymmetric digital signature, mutate the state, and verify the new hash.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If an attacker tries to skip a screen, inject a fake discount, or replay an old state, the hash chain breaks. The state machine fails fast before a single pixel is rendered or a network call is fired.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Security Concern &lt;/th&gt;&lt;th&gt;Standard REST + HTTPS &lt;/th&gt;&lt;th&gt;JWT Tokens &lt;/th&gt;&lt;th&gt;TrustChain&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data In-Transit Encryption&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Yes (TLS) &lt;/td&gt;&lt;td&gt;Yes (TLS) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes (TLS)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Prevent Step Skipping (Deep Links)&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No (Complex backend checks) &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes (Parent Hash Check)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Detect Tampered State Deltas&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Partially &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes (Asymmetric Signatures)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Replay Attack Prevention&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Manual nonces required &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes (Strict Hash Lineage)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Zero Runtime Dependencies&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;N/A &lt;/td&gt;&lt;td&gt;Often heavy &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes (Pure KMP)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;designing-for-zero-bloat-the-core-primitives&quot;&gt;Designing for Zero Bloat: The Core Primitives&lt;/h3&gt;
&lt;p&gt;One of my main goals with &lt;code&gt;TrustChain&lt;/code&gt; was keeping the core completely free of third-party runtime dependencies so it drops cleanly into Android, iOS, Desktop, Web, or backend KMP targets.&lt;/p&gt;
&lt;h4 id=&quot;1-verifiableblock&quot;&gt;1. &lt;code&gt;VerifiableBlock&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Any screen, workflow step, or state snapshot in your domain simply implements a three-property interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;interface VerifiableBlock {
    val id: String
    val hash: String
    val previousHash: String? // null only for the root (genesis) step
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-deltablock-signed-partial-updates-&quot;&gt;2. &lt;code&gt;DeltaBlock&lt;/code&gt; (Signed Partial Updates)&lt;/h4&gt;
&lt;p&gt;Why re-fetch an entire screen when a user applies a 10% coupon or an order changes status? Instead of sending full state trees, the server sends a lightweight &lt;code&gt;DeltaBlock&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Serializable
data class DeltaBlock&amp;lt;P&amp;gt;(
    @SerialName(&quot;patches&quot;) val patches: P,
    @SerialName(&quot;previous_hash&quot;) val previousHash: String,
    @SerialName(&quot;new_hash&quot;) val newHash: String,
    @SerialName(&quot;signature&quot;) val signature: String = &quot;&quot;
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how &lt;code&gt;DeltaBlock&lt;/code&gt; locks the mutation from both sides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can &lt;strong&gt;only&lt;/strong&gt; be applied if the client is currently sitting on &lt;code&gt;previousHash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Its &lt;code&gt;newHash&lt;/code&gt; is signed by the server's private key (&lt;code&gt;RSA&lt;/code&gt;, &lt;code&gt;Ed25519&lt;/code&gt;, or &lt;code&gt;ECDSA&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Once the client applies &lt;code&gt;patches&lt;/code&gt; via a &lt;code&gt;BlockPatcher&lt;/code&gt;, the resulting block must produce &lt;code&gt;newHash&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-pure-functional-reduction&quot;&gt;3. Pure Functional Reduction&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;TrustChain&lt;/code&gt; is completely immutable. There are no hidden side effects, mutable singletons, or uncaught exceptions flying across coroutines. You send a &lt;code&gt;ChainEvent&lt;/code&gt; (&lt;code&gt;Push&lt;/code&gt;, &lt;code&gt;Pop&lt;/code&gt;, &lt;code&gt;Replace&lt;/code&gt;, or &lt;code&gt;ApplyDeltas&lt;/code&gt;), and &lt;code&gt;reduce()&lt;/code&gt; returns a standard Kotlin &lt;code&gt;Result&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface ChainEvent&amp;lt;out B : VerifiableBlock, out P&amp;gt; {
    data class Push&amp;lt;B : VerifiableBlock&amp;gt;(val block: B) : ChainEvent&amp;lt;B, Nothing&amp;gt;
    data object Pop : ChainEvent&amp;lt;Nothing, Nothing&amp;gt;
    data class Replace&amp;lt;B : VerifiableBlock&amp;gt;(val block: B) : ChainEvent&amp;lt;B, Nothing&amp;gt;
    data class ApplyDeltas&amp;lt;P&amp;gt;(val blocks: List&amp;lt;DeltaBlock&amp;lt;P&amp;gt;&amp;gt;) : ChainEvent&amp;lt;Nothing, P&amp;gt;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a batch of 5 deltas is applied and the 4th delta has a broken hash link or invalid signature, &lt;strong&gt;atomicity is preserved&lt;/strong&gt; — the entire reduction fails cleanly with a typed &lt;code&gt;ChainError&lt;/code&gt;, leaving your original &lt;code&gt;TrustChain&lt;/code&gt; untouched.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-walkthrough-the-unbreakable-checkout&quot;&gt;Real-World Walkthrough: The Unbreakable Checkout&lt;/h3&gt;
&lt;p&gt;Let’s look at how this works in practice using a multi-step checkout flow.&lt;/p&gt;
&lt;p&gt;First, we define our domain step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Serializable
data class CheckoutStep(
    override val id: String,
    val title: String,
    val state: Map&amp;lt;String, String&amp;gt; = emptyMap(),
    override val hash: String,
    override val previousHash: String? = null
) : VerifiableBlock

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we initialize &lt;code&gt;TrustChain&lt;/code&gt; on the client. Notice how cryptography and state patching are injected via simple functional interfaces (&lt;code&gt;SignatureVerifier&lt;/code&gt; and &lt;code&gt;BlockPatcher&lt;/code&gt;). You aren't locked into any specific crypto library:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;var clientChain = TrustChain&amp;lt;CheckoutStep, Map&amp;lt;String, String&amp;gt;&amp;gt;(
    strictMode = true,
    verifier = SignatureVerifier { payload, signatureBase64 -&amp;gt;
        // Plug in your platform's Ed25519 / RSA / ECDSA verifier
        CryptoEngine.verify(serverPublicKey, payload, signatureBase64)
    },
    patcher = { step, patches, newHash -&amp;gt;
        step.copy(
            state = step.state + patches,
            hash = newHash
        )
    }
)

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-legitimate-user-journey&quot;&gt;The Legitimate User Journey&lt;/h4&gt;
&lt;p&gt;As the user progresses through the checkout, every step returned by the server references the exact hash of the previous state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// 1. User opens the Shopping Cart (Genesis block, previousHash = null)
val cartStep = server.startCheckout(sessionId)
clientChain = clientChain.reduce(ChainEvent.Push(cartStep)).getOrThrow()
// Active: Shopping Cart | Hash: h_c2c1a28598b8ad69

// 2. User confirms Shipping Address (Chained to Cart's hash)
val shippingStep = server.submitShipping(sessionId, &quot;Baker St 221B&quot;)
clientChain = clientChain.reduce(ChainEvent.Push(shippingStep)).getOrThrow()
// Active: Shipping &amp;amp; Address | Hash: h_50bda3ba68cced0d | Prev: h_c2c1a28598b8ad69

// 3. User applies a 10% Promo Code (Signed DeltaBlock mutation)
val promoDelta = server.applyPromoCode(sessionId, &quot;10%&quot;)
clientChain = clientChain.reduce(ChainEvent.ApplyDeltas(listOf(promoDelta))).getOrThrow()
// Active: Shipping &amp;amp; Address (Patched) | New Total: $1890 | New Hash: h_6471c5f0c53a644b

// 4. User proceeds to Payment (Chained to the patched Shipping hash)
val paymentStep = server.proceedToPayment(sessionId)
clientChain = clientChain.reduce(ChainEvent.Push(paymentStep)).getOrThrow()
// Active: Payment Authorization | Hash: h_6a6fd155e392f7ef

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;simulating-the-attack-skipping-shipping&quot;&gt;Simulating the Attack: Skipping Shipping&lt;/h4&gt;
&lt;p&gt;Now, let’s see what happens when an attacker loads the Cart (&lt;code&gt;h_c2c1a28598b8ad69&lt;/code&gt;) and tries to jump straight to the &lt;code&gt;paymentStep&lt;/code&gt; (bypassing shipping calculation):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;var attackedChain = TrustChain&amp;lt;CheckoutStep, Map&amp;lt;String, String&amp;gt;&amp;gt;(strictMode = true)
attackedChain = attackedChain.reduce(ChainEvent.Push(cartStep)).getOrThrow()

// Attacker tries to force-push the Payment step directly onto the Cart
val attackResult = attackedChain.reduce(ChainEvent.Push(paymentStep))

attackResult.fold(
    onSuccess = {
        println(&quot;VULNERABILITY: Attack succeeded!&quot;)
    },
    onFailure = { error -&amp;gt;
        when (error) {
            is ChainError.HashMismatch -&amp;gt; {
                println(&quot;[!] BLOCKED BY TRUSTCHAIN: State machine integrity preserved.&quot;)
                println(&quot;    Expected parent: ${error.expected}, Actual: ${error.actual}&quot;)
            }
            is ChainError.SignatureVerificationFailed -&amp;gt; {
                println(&quot;[!] BLOCKED: Invalid server signature for block ${error.blockHash}&quot;)
            }
            else -&amp;gt; println(&quot;[!] Blocked: ${error.message}&quot;)
        }
    }
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Console Output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;------------------------------------------------------
  Simulating Attack: Attacker tries to skip shipping  
------------------------------------------------------
Attacker has Cart loaded.
Attacker attempts to directly apply Payment step (bypassing Shipping)...
[!] BLOCKED BY TRUSTCHAIN: State machine integrity preserved.
Expected parent: h_c2c1a28598b8ad69, Actual: h_6471c5f0c53a644b

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;paymentStep&lt;/code&gt; expects its parent to be the patched shipping step (&lt;code&gt;h_6471c5f0c53a644b&lt;/code&gt;), pushing it onto the cart (&lt;code&gt;h_c2c1a28598b8ad69&lt;/code&gt;) triggers an immediate &lt;code&gt;ChainError.HashMismatch&lt;/code&gt;. The state transition is rejected atomically.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;built-for-developer-ergonomics&quot;&gt;Built for Developer Ergonomics&lt;/h3&gt;
&lt;p&gt;Security tools often fail because they make local development painful. Nobody wants to spin up a signing server just to preview a Compose Multiplatform screen or write a quick UI test.&lt;/p&gt;
&lt;p&gt;That’s why &lt;code&gt;TrustChain&lt;/code&gt; includes a &lt;code&gt;strictMode&lt;/code&gt; flag:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;strictMode = true&lt;/code&gt; (Default / Production):&lt;/strong&gt; Enforces parent hash lineage on &lt;code&gt;Push&lt;/code&gt; and &lt;code&gt;Replace&lt;/code&gt;, verifies digital signatures via &lt;code&gt;SignatureVerifier&lt;/code&gt;, and ensures &lt;code&gt;BlockPatcher&lt;/code&gt; updates block hashes accurately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;strictMode = false&lt;/code&gt; (Previews / UI Prototyping):&lt;/strong&gt; Bypasses cryptographic and lineage checks while keeping the exact same &lt;code&gt;ChainEvent&lt;/code&gt; reducer API, allowing you to mock and jump between screens freely during development.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-does-trustchain-shine-&quot;&gt;Where Does TrustChain Shine?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server-Driven UI (SDUI):&lt;/strong&gt; Guarantee that screens and dynamic patches pushed by the backend are rendered in the exact sequence intended by the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E-Commerce &amp;amp; Checkouts:&lt;/strong&gt; Make it impossible to bypass delivery calculation, tax steps, or terms acceptance before payment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fintech &amp;amp; Banking:&lt;/strong&gt; Enforce strict step ordering for wire transfers, credit limit increases, and multi-factor challenges.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KYC &amp;amp; Onboarding Funnels:&lt;/strong&gt; Maintain a verifiable, tamper-proof progression through document capture and liveness checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-in-your-project&quot;&gt;Try It in Your Project&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TrustChain&lt;/strong&gt; is open-source under the Apache 2.0 License and published directly to Maven Central.&lt;/p&gt;
&lt;p&gt;Check out the full source code, architecture docs, and the runnable CLI attack simulation on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/trustchain&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31019/https-wont-save-your-checkout-flow-building-zero-trust-state-machines-kotlin-multiplatform</guid>
<pubDate>Wed, 14 Oct 2026 06:29:28 +0000</pubDate>
</item>
<item>
<title>From C# Backend to Android: Why My First Mobile App in Kotlin Clicked</title>
<link>https://coderlegion.com/30785/from-c-backend-to-android-why-my-first-mobile-app-in-kotlin-clicked</link>
<description>&lt;p&gt;Moving from backend C# and enterprise patterns into native mobile development can feel like stepping into a whole new world. But after building my very first mobile app using Kotlin, the experience was a revelation—the syntax felt natural, the developer tooling was sharp, and the results were immediate.&lt;/p&gt;
&lt;p&gt;Coming from a C# background, Kotlin doesn't feel foreign at all; it feels like an alternative timeline of modern object-oriented and functional programming.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-benefits-that-stand-out-for-c-developers&quot;&gt;Key Benefits That Stand Out for C# Developers&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Familiar Null Safety:&lt;/strong&gt; Just like C#'s nullable reference types, Kotlin barks at nullability at compile time. Using the &lt;code&gt;?&lt;/code&gt; operator and safe-call operators keeps runtime crashes like &lt;code&gt;NullPointerException&lt;/code&gt; (the Java equivalent of &lt;code&gt;NullReferenceException&lt;/code&gt;) off your production apps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coroutines vs. Async/Await:&lt;/strong&gt; If you are used to C#'s &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; pattern and channels, Kotlin's coroutines feel instantly intuitive. They make non-blocking UI rendering, API fetching, and background processing clean, sequential, and easy to reason about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Radical Conciseness with Data Classes:&lt;/strong&gt; Where a C# record or class with properties can still require a few lines, Kotlin's &lt;code&gt;data class&lt;/code&gt; automatically generates &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, and &lt;code&gt;toString()&lt;/code&gt; methods in a single line of code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extension Functions:&lt;/strong&gt; Just like C# extension methods, Kotlin lets you add new behavior to existing classes without inheritance or boilerplate decorator wrappers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern UI Development:&lt;/strong&gt; Paired with Jetpack Compose (or Compose Multiplatform), Kotlin allows you to build declarative user interfaces—similar to modern Web or XAML/MAUI approaches—making responsive layouts for different device sizes straightforward.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-technical-references-for-c-developers&quot;&gt;Key Technical References for C# Developers&lt;/h4&gt;
&lt;p&gt;When diving deeper into Kotlin from .NET, the following official references and standards are worth bookmarking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language Specification &amp;amp; Core Standards:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/api/core/kotlin-stdlib/&quot;&gt;Kotlin Standard Library API Reference (&lt;code&gt;kotlin-stdlib&lt;/code&gt;)&lt;/a&gt;: Provides core data structures, scoping functions (&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;apply&lt;/code&gt;, &lt;code&gt;also&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;), and extension functions.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/spec/kotlin-spec.html&quot;&gt;Kotlin Language Specification&lt;/a&gt;: Covers fundamental compiler behaviors, type inference, subtyping, and execution semantics.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concurrency &amp;amp; Jetpack Tooling:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot;&gt;Kotlin Coroutines Specification &amp;amp; Guide&lt;/a&gt;: Formal reference for structured concurrency, flows, and channels.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.android.com/jetpack/compose&quot;&gt;Android Jetpack Compose Documentation&lt;/a&gt;: Official toolkit for building native Android UIs declaratively with Kotlin.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Building that first mobile app proves that backend principles—clean architecture, strong typing, and smart asynchronous design—translate directly into mobile development. Kotlin bridges the gap perfectly, giving C# developers a powerful, highly productive stack for native mobile apps.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30785/from-c-backend-to-android-why-my-first-mobile-app-in-kotlin-clicked</guid>
<pubDate>Mon, 12 Oct 2026 08:10:01 +0000</pubDate>
</item>
<item>
<title>Building Payment Flows in Android: Lessons from Real Fintech Apps</title>
<link>https://coderlegion.com/28306/building-payment-flows-in-android-lessons-from-real-fintech-apps</link>
<description>&lt;h3 id=&quot;building-payment-flows-in-android-production-patterns-from-fintech-applications&quot;&gt;Building Payment Flows in Android: Production Patterns from Fintech Applications&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; Architectural patterns and state management strategies for reliable payment flows at scale&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Over the past months, I've been building and refining payment systems at scale. Every architectural decision compounds over time, and payment flows demand particular attention to state management, error handling, and offline resilience.&lt;/p&gt;
&lt;p&gt;This article documents the patterns that have proven effective in production fintech applications. These aren't theoretical approaches—they represent lessons learned from handling millions of transactions and the edge cases that accompany them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-payment-flows-require-specialized-architecture&quot;&gt;Why Payment Flows Require Specialized Architecture&lt;/h3&gt;
&lt;p&gt;Most mobile applications tolerate a certain degree of architectural looseness. A user can restart the app if something fails. Recovery is straightforward.&lt;/p&gt;
&lt;p&gt;Payment flows operate under different constraints:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State Ambiguity is Costly&lt;/strong&gt;&lt;br&gt;
When a user initiates a payment, your application must maintain absolute clarity about its status: submitted, pending, failed, queued offline. If this state is unclear, the user is equally uncertain, leading to duplicate submissions or lost transactions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Race Conditions Have Financial Consequences&lt;/strong&gt;&lt;br&gt;
A user confirms payment and immediately navigates away. Your retry logic must account for this without resubmitting the payment or losing the transaction record.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network Reliability Cannot Be Assumed&lt;/strong&gt;&lt;br&gt;
In many markets, network interruptions occur mid-transaction. Your system must handle incomplete requests gracefully—queuing the transaction locally and retrying when connectivity returns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Offline-First Is Essential&lt;/strong&gt;&lt;br&gt;
Users may initiate transactions without active connectivity. The application must queue payments locally, survive process termination, and submit reliably when network becomes available.&lt;/p&gt;
&lt;p&gt;This is why Clean Architecture and MVI pattern implementation is not optional for payment flows—it becomes a structural guarantee that state remains traceable and predictable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architectural-foundation&quot;&gt;Architectural Foundation&lt;/h3&gt;
&lt;p&gt;For context on the underlying architecture, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@nerojust4/clean-architecture-in-android-a-real-world-guide-2025-edition-e5b4e950674c&quot;&gt;Clean Architecture in Android: A Real-World Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The structure we use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;:domain&lt;/strong&gt; — Pure Kotlin business logic without Android dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;:data&lt;/strong&gt; — Repository layer managing offline queues, network calls, and persistence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;:feature:payment&lt;/strong&gt; — MVI implementation containing Intent, UiState, ViewModel, and UI layers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation ensures business logic can be tested independently of Android runtime, catching state-related bugs before deployment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pattern-1-payment-confirmation-flow-with-explicit-state&quot;&gt;Pattern 1: Payment Confirmation Flow with Explicit State&lt;/h3&gt;
&lt;h4 id=&quot;the-mvi-contract&quot;&gt;The MVI Contract&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed class PaymentConfirmationIntent {
    data class LoadPaymentDetails(val paymentId: String) : PaymentConfirmationIntent()
    object ConfirmPayment : PaymentConfirmationIntent()
    object RetryPayment : PaymentConfirmationIntent()
    object CancelPayment : PaymentConfirmationIntent()
    object DismissError : PaymentConfirmationIntent()
}

data class PaymentConfirmationUiState(
    val paymentDetails: PaymentDetails? = null,
    val isLoading: Boolean = false,
    val isConfirming: Boolean = false,
    val error: PaymentError? = null,
    val isOffline: Boolean = false,
    val confirmationStatus: ConfirmationStatus = ConfirmationStatus.Idle,
) {
    enum class ConfirmationStatus {
        Idle,
        AwaitingUserConfirmation,
        Processing,
        Success,
        Failed,
        OfflineQueued,
    }
}

sealed class PaymentConfirmationEffect {
    object NavigateToSuccess : PaymentConfirmationEffect()
    object NavigateToFailure : PaymentConfirmationEffect()
    object NavigateBack : PaymentConfirmationEffect()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Structural Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ConfirmationStatus.OfflineQueued&lt;/code&gt; state explicitly represents the condition where payment has been submitted locally and awaits network availability. This eliminates ambiguity about whether a transaction was successfully queued.&lt;/p&gt;
&lt;p&gt;Each &lt;code&gt;Intent&lt;/code&gt; represents a discrete user action with no overlap, preventing state collisions. The &lt;code&gt;isOffline&lt;/code&gt; flag as part of UiState ensures the UI layer can respond to network conditions with appropriate messaging.&lt;/p&gt;
&lt;h4 id=&quot;viewmodel-implementation&quot;&gt;ViewModel Implementation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@HiltViewModel
class PaymentConfirmationViewModel @Inject constructor(
    private val paymentRepository: PaymentRepository,
    private val offlinePaymentQueue: OfflinePaymentQueue,
    private val networkManager: NetworkManager,
) : ViewModel() {

    private val _state = MutableStateFlow&amp;lt;PaymentConfirmationUiState&amp;gt;(
        PaymentConfirmationUiState()
    )
    val state: StateFlow&amp;lt;PaymentConfirmationUiState&amp;gt; = _state.asStateFlow()

    fun processIntent(intent: PaymentConfirmationIntent) {
        when (intent) {
            is PaymentConfirmationIntent.LoadPaymentDetails -&amp;gt; 
                loadPaymentDetails(intent.paymentId)
            is PaymentConfirmationIntent.ConfirmPayment -&amp;gt; 
                confirmPayment()
            is PaymentConfirmationIntent.RetryPayment -&amp;gt; 
                retryPayment()
            is PaymentConfirmationIntent.CancelPayment -&amp;gt; 
                cancelPayment()
            is PaymentConfirmationIntent.DismissError -&amp;gt; 
                dismissError()
        }
    }

    private fun confirmPayment() {
        viewModelScope.launch {
            _state.update { it.copy(isConfirming = true) }

            try {
                if (!networkManager.isConnected()) {
                    val queuedPayment = offlinePaymentQueue.enqueue(
                        _state.value.paymentDetails ?: return@launch
                    )
                    _state.update {
                        it.copy(
                            isConfirming = false,
                            confirmationStatus = 
                                PaymentConfirmationUiState.ConfirmationStatus.OfflineQueued,
                        )
                    }
                    return@launch
                }

                val result = paymentRepository.submitPayment(
                    _state.value.paymentDetails!!
                )

                when (result) {
                    is PaymentResult.Success -&amp;gt; {
                        _state.update {
                            it.copy(
                                isConfirming = false,
                                confirmationStatus = 
                                    PaymentConfirmationUiState.ConfirmationStatus.Success,
                            )
                        }
                    }
                    is PaymentResult.Failure -&amp;gt; {
                        _state.update {
                            it.copy(
                                isConfirming = false,
                                error = result.error,
                                confirmationStatus = 
                                    PaymentConfirmationUiState.ConfirmationStatus.Failed,
                            )
                        }
                    }
                }
            } catch (e: Exception) {
                _state.update {
                    it.copy(
                        isConfirming = false,
                        error = PaymentError.Unknown(e.message ?: &quot;Unknown error&quot;),
                        confirmationStatus = 
                            PaymentConfirmationUiState.ConfirmationStatus.Failed,
                    )
                }
            }
        }
    }

    private fun retryPayment() {
        confirmPayment()
    }

    private fun dismissError() {
        _state.update { it.copy(error = null) }
    }

    private fun loadPaymentDetails(paymentId: String) {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }
            try {
                val details = paymentRepository.getPaymentDetails(paymentId)
                _state.update {
                    it.copy(
                        isLoading = false,
                        paymentDetails = details,
                        isOffline = !networkManager.isConnected(),
                    )
                }
            } catch (e: Exception) {
                _state.update {
                    it.copy(
                        isLoading = false,
                        error = PaymentError.LoadFailed(
                            e.message ?: &quot;Failed to load&quot;
                        ),
                    )
                }
            }
        }
    }

    private fun cancelPayment() {
        // Navigate back without submission
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key Implementation Details:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Network connectivity is checked before submission&lt;/li&gt;
&lt;li&gt;Offline state triggers local queueing with explicit status update&lt;/li&gt;
&lt;li&gt;Exception handling updates UI state rather than propagating errors&lt;/li&gt;
&lt;li&gt;All state transitions are explicit and traceable&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;ui-layer&quot;&gt;UI Layer&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun PaymentConfirmationScreen(
    viewModel: PaymentConfirmationViewModel,
    paymentId: String,
    onSuccess: () -&amp;gt; Unit,
    onFailure: () -&amp;gt; Unit,
    onBack: () -&amp;gt; Unit,
) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.processIntent(
            PaymentConfirmationIntent.LoadPaymentDetails(paymentId)
        )
    }

    LaunchedEffect(state.confirmationStatus) {
        when (state.confirmationStatus) {
            PaymentConfirmationUiState.ConfirmationStatus.Success -&amp;gt; 
                onSuccess()
            PaymentConfirmationUiState.ConfirmationStatus.Failed -&amp;gt; 
                onFailure()
            else -&amp;gt; {}
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        when {
            state.isLoading -&amp;gt; {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.Center)
                )
            }
            state.paymentDetails != null -&amp;gt; {
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(16.dp),
                    verticalArrangement = Arrangement.spacedBy(16.dp),
                ) {
                    PaymentDetailsCard(state.paymentDetails)

                    if (state.isOffline) {
                        OfflineWarningBanner()
                    }

                    state.error?.let { error -&amp;gt;
                        ErrorBanner(
                            message = error.message,
                            onDismiss = {
                                viewModel.processIntent(
                                    PaymentConfirmationIntent.DismissError
                                )
                            }
                        )
                    }

                    Spacer(modifier = Modifier.weight(1f))

                    when (state.confirmationStatus) {
                        PaymentConfirmationUiState.ConfirmationStatus
                            .AwaitingUserConfirmation -&amp;gt; {
                            ConfirmationButtons(viewModel, onBack)
                        }
                        PaymentConfirmationUiState.ConfirmationStatus
                            .OfflineQueued -&amp;gt; {
                            Button(
                                onClick = { onBack() },
                                modifier = Modifier.fillMaxWidth(),
                            ) {
                                Text(&quot;Done&quot;)
                            }
                        }
                        PaymentConfirmationUiState.ConfirmationStatus.Failed -&amp;gt; {
                            Button(
                                onClick = {
                                    viewModel.processIntent(
                                        PaymentConfirmationIntent.RetryPayment
                                    )
                                },
                                modifier = Modifier.fillMaxWidth(),
                            ) {
                                Text(&quot;Retry&quot;)
                            }
                        }
                        else -&amp;gt; {}
                    }
                }
            }
            state.error != null -&amp;gt; {
                ErrorScreen(
                    error = state.error,
                    onRetry = {
                        viewModel.processIntent(
                            PaymentConfirmationIntent
                                .LoadPaymentDetails(paymentId)
                        )
                    },
                    onBack = onBack,
                )
            }
        }
    }
}

@Composable
private fun OfflineWarningBanner() {
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFFFFF3CD)
        ),
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(12.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Icon(
                imageVector = Icons.Default.Warning,
                contentDescription = null,
                tint = Color(0xFFFF9800),
            )
            Text(
                text = &quot;You are offline. Your payment will be submitted when connectivity is restored.&quot;,
                fontSize = 14.sp,
                modifier = Modifier.weight(1f),
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pattern-2-offline-payment-queue-with-persistence&quot;&gt;Pattern 2: Offline Payment Queue with Persistence&lt;/h3&gt;
&lt;p&gt;Local queueing with durable storage is essential for offline-first applications.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// domain/payment/OfflinePaymentQueue.kt

data class QueuedPayment(
    val id: String,
    val payment: PaymentDetails,
    val timestamp: Long,
    val retryCount: Int = 0,
    val status: QueueStatus = QueueStatus.Pending,
) {
    enum class QueueStatus {
        Pending,
        Retrying,
        Submitted,
        Failed,
    }
}

interface OfflinePaymentQueue {
    suspend fun enqueue(payment: PaymentDetails): QueuedPayment
    suspend fun getAll(): List&amp;lt;QueuedPayment&amp;gt;
    suspend fun getById(id: String): QueuedPayment?
    suspend fun remove(id: String)
    suspend fun updateStatus(id: String, status: QueueStatus)
    suspend fun incrementRetry(id: String)
}

// data/payment/OfflinePaymentQueueImpl.kt

@Singleton
class OfflinePaymentQueueImpl @Inject constructor(
    private val paymentDao: PaymentQueueDao,
) : OfflinePaymentQueue {

    override suspend fun enqueue(payment: PaymentDetails): QueuedPayment {
        val queued = QueuedPayment(
            id = UUID.randomUUID().toString(),
            payment = payment,
            timestamp = System.currentTimeMillis(),
        )
        paymentDao.insert(queued.toEntity())
        return queued
    }

    override suspend fun getAll(): List&amp;lt;QueuedPayment&amp;gt; {
        return paymentDao.getAll().map { it.toDomain() }
    }

    override suspend fun updateStatus(
        id: String,
        status: QueueStatus
    ) {
        paymentDao.updateStatus(id, status.name)
    }

    override suspend fun remove(id: String) {
        paymentDao.delete(id)
    }

    override suspend fun incrementRetry(id: String) {
        paymentDao.incrementRetry(id)
    }

    override suspend fun getById(id: String): QueuedPayment? {
        return paymentDao.getById(id)?.toDomain()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Background synchronization handles submission when connectivity is restored:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// data/payment/PaymentSyncWorker.kt

class PaymentSyncWorker @Inject constructor(
    context: Context,
    params: WorkerParameters,
    private val offlineQueue: OfflinePaymentQueue,
    private val paymentRepository: PaymentRepository,
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            val queuedPayments = offlineQueue.getAll()
                .filter { it.status == QueueStatus.Pending }

            queuedPayments.forEach { queued -&amp;gt;
                try {
                    offlineQueue.updateStatus(
                        queued.id,
                        QueueStatus.Retrying
                    )

                    val result = paymentRepository.submitPayment(
                        queued.payment
                    )

                    when (result) {
                        is PaymentResult.Success -&amp;gt; {
                            offlineQueue.updateStatus(
                                queued.id,
                                QueueStatus.Submitted
                            )
                            offlineQueue.remove(queued.id)
                        }
                        is PaymentResult.Failure -&amp;gt; {
                            if (queued.retryCount &amp;lt; MAX_RETRIES) {
                                offlineQueue.incrementRetry(queued.id)
                            } else {
                                offlineQueue.updateStatus(
                                    queued.id,
                                    QueueStatus.Failed
                                )
                            }
                        }
                    }
                } catch (e: Exception) {
                    if (queued.retryCount &amp;lt; MAX_RETRIES) {
                        offlineQueue.incrementRetry(queued.id)
                    } else {
                        offlineQueue.updateStatus(
                            queued.id,
                            QueueStatus.Failed
                        )
                    }
                }
            }

            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }

    companion object {
        private const val MAX_RETRIES = 5
        const val WORK_NAME = &quot;payment_sync&quot;
    }
}

// Schedule in application startup:
// WorkManager.getInstance(context).enqueueUniquePeriodicWork(
//     PaymentSyncWorker.WORK_NAME,
//     ExistingPeriodicWorkPolicy.KEEP,
//     PeriodicWorkRequestBuilder&amp;lt;PaymentSyncWorker&amp;gt;(
//         15, TimeUnit.MINUTES
//     ).build()
// )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Benefits of This Approach:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Payment data persists across process termination&lt;/li&gt;
&lt;li&gt;Retry logic respects maximum attempt limits&lt;/li&gt;
&lt;li&gt;Each payment has a unique identifier preventing duplicate submissions&lt;/li&gt;
&lt;li&gt;Status transitions are durable and traceable&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pattern-3-biometric-authentication-for-payment-confirmation&quot;&gt;Pattern 3: Biometric Authentication for Payment Confirmation&lt;/h3&gt;
&lt;p&gt;Biometric authentication integrates as a discrete state within the payment flow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed class BiometricAuthIntent {
    object AuthenticateWithBiometric : BiometricAuthIntent()
    object CancelBiometric : BiometricAuthIntent()
    object RetryBiometric : BiometricAuthIntent()
}

data class BiometricAuthUiState(
    val isAuthenticating: Boolean = false,
    val authResult: BiometricAuthResult? = null,
    val error: BiometricError? = null,
) {
    sealed class BiometricAuthResult {
        object Success : BiometricAuthResult()
        object Failure : BiometricAuthResult()
        object Cancelled : BiometricAuthResult()
    }
}

@HiltViewModel
class BiometricAuthViewModel @Inject constructor(
    private val biometricManager: BiometricManager,
) : ViewModel() {

    private val _state = MutableStateFlow&amp;lt;BiometricAuthUiState&amp;gt;(
        BiometricAuthUiState()
    )
    val state: StateFlow&amp;lt;BiometricAuthUiState&amp;gt; = _state.asStateFlow()

    fun processIntent(intent: BiometricAuthIntent) {
        when (intent) {
            BiometricAuthIntent.AuthenticateWithBiometric -&amp;gt; authenticate()
            BiometricAuthIntent.CancelBiometric -&amp;gt; cancel()
            BiometricAuthIntent.RetryBiometric -&amp;gt; authenticate()
        }
    }

    private fun authenticate() {
        viewModelScope.launch {
            _state.update { it.copy(isAuthenticating = true) }

            try {
                val result = biometricManager.authenticate()
                _state.update {
                    it.copy(
                        isAuthenticating = false,
                        authResult = result,
                    )
                }
            } catch (e: BiometricException) {
                _state.update {
                    it.copy(
                        isAuthenticating = false,
                        error = BiometricError.AuthenticationFailed(
                            e.message ?: &quot;Authentication failed&quot;
                        ),
                    )
                }
            }
        }
    }

    private fun cancel() {
        biometricManager.cancel()
        _state.update {
            it.copy(
                isAuthenticating = false,
                authResult = BiometricAuthUiState.BiometricAuthResult
                    .Cancelled,
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Biometric authentication state remains consistent with the MVI pattern rather than introducing separate concerns.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;transaction-flow-example&quot;&gt;Transaction Flow Example&lt;/h3&gt;
&lt;p&gt;Consider a complete transaction lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User initiates payment confirmation&lt;/li&gt;
&lt;li&gt;App checks network connectivity&lt;/li&gt;
&lt;li&gt;If offline: payment queued locally with &lt;code&gt;OfflineQueued&lt;/code&gt; status&lt;/li&gt;
&lt;li&gt;If online: payment submitted to server&lt;/li&gt;
&lt;li&gt;On network restoration: background worker processes offline queue&lt;/li&gt;
&lt;li&gt;Each queued payment retried up to 5 times before marking failed&lt;/li&gt;
&lt;li&gt;User notified of final status through UI state updates&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach ensures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No state ambiguity at any point&lt;/li&gt;
&lt;li&gt;Network interruptions don't result in lost transactions&lt;/li&gt;
&lt;li&gt;Duplicate submissions are prevented&lt;/li&gt;
&lt;li&gt;Process termination doesn't destroy payment records&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;open-source-implementation&quot;&gt;Open-Source Implementation&lt;/h3&gt;
&lt;p&gt;These patterns are available in production-ready form:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nerojust/clean-architecture-android&quot;&gt;clean-architecture-android&lt;/a&gt;&lt;/strong&gt; — Foundation template with Clean Architecture, MVI, and Jetpack Compose&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nerojust/fintech-mobile-patterns-android&quot;&gt;fintech-mobile-patterns-android&lt;/a&gt;&lt;/strong&gt; — Payment flow implementations with offline queuing, error handling, and authentication patterns&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both repositories include complete implementations with testing examples.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Payment flow architecture demands explicit state management and thoughtful error handling. The patterns discussed—explicit state enums, offline queuing with persistence, and MVI-based architecture—provide structural guarantees that reduce the likelihood of state-related bugs.&lt;/p&gt;
&lt;p&gt;These approaches have proven effective at production scale. They prioritize clarity of state over brevity of code, understanding that payment systems benefit more from predictability than elegance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@nerojust4/clean-architecture-in-android-a-real-world-guide-2025-edition-e5b4e950674c&quot;&gt;Clean Architecture in Android: A Real-World Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@nerojust4/mastering-dependency-injection-with-hilt-in-android-81b3d221da9a&quot;&gt;Mastering Dependency Injection with Hilt in Android&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/nyerhovwo-adjekughene-97508674&quot;&gt;LinkedIn&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nerojust&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28306/building-payment-flows-in-android-lessons-from-real-fintech-apps</guid>
<pubDate>Wed, 23 Sep 2026 09:35:18 +0000</pubDate>
</item>
<item>
<title>Android CI/CD for Beginners — Part 1: Introduction</title>
<link>https://coderlegion.com/28201/android-ci-cd-for-beginners-part-1-introduction</link>
<description>&lt;p&gt;Stop building everything manually. Learn how modern Android teams automate their development workflow&lt;/p&gt;
&lt;p&gt;This series is designed to provide a complete understanding of CI/CD for Android development. In Part 1, we’ll introduce the fundamentals of CI/CD, explore what it is, why it matters, and how it’s used in Android app development.&lt;/p&gt;
&lt;p&gt;CI/CD (Continuous Integration and Continuous Delivery/Deployment) is a set of practices that automate repetitive tasks in the software development process, such as building, testing, and releasing applications.&lt;/p&gt;
&lt;p&gt;Instead of manually performing these tasks every time you make a change, a CI/CD pipeline executes them automatically whenever predefined events occur, such as pushing code to a Git repository.&lt;/p&gt;
&lt;p&gt;How can CI/CD automate tasks in Android development?&lt;br&gt;
CI/CD automates many of the repetitive tasks involved in the Android development lifecycle. Instead of running these steps manually every time you make a change, they can be executed automatically whenever code is pushed to your repository.&lt;/p&gt;
&lt;p&gt;With a CI/CD pipeline, your Android app can be automatically:&lt;/p&gt;
&lt;p&gt;Built using Gradle.&lt;br&gt;
Analyzed for code quality with Android Lint.&lt;br&gt;
Tested by running unit and instrumentation tests.&lt;br&gt;
Packaged into a signed or unsigned APK or Android App Bundle (AAB).&lt;br&gt;
Distributed to testers or prepared for release to the Google Play Store.&lt;br&gt;
You might be wondering: Why do we need to automate these tasks?&lt;/p&gt;
&lt;p&gt;In real-world Android projects, multiple developers often work on the same codebase. Every new feature, bug fix, or improvement is usually submitted as a pull request (PR).&lt;/p&gt;
&lt;p&gt;Before those changes are merged, the team needs to ensure they meet the project’s quality standards and don’t introduce bugs or break existing functionality.&lt;/p&gt;
&lt;p&gt;Manually building the app, running lint checks, executing tests, and verifying every pull request can quickly become repetitive and time-consuming, especially as the project grows.&lt;/p&gt;
&lt;p&gt;This is where CI/CD becomes valuable. Whenever a pull request is opened or updated, a CI pipeline can automatically build the app, run lint checks, execute unit and instrumentation tests, and report whether all checks have passed.&lt;/p&gt;
&lt;p&gt;If any step fails, developers are notified immediately, allowing issues to be fixed before the changes are merged.&lt;/p&gt;
&lt;p&gt;Once all the automated checks succeed, the CD pipeline can take over by generating an APK or App Bundle and distributing it to testers or preparing it for release.&lt;/p&gt;
&lt;p&gt;This eliminates many manual steps, speeds up the development process, and helps maintain a stable and reliable codebase.&lt;/p&gt;
&lt;p&gt;Common CI/CD Terminology&lt;br&gt;
Before we start building our first CI/CD pipeline, it’s important to understand some common terms you’ll encounter throughout this series. These concepts are not exclusive to Android development, they’re used across many CI/CD platforms. We’ll explore how these ideas come together in practice in the upcoming sections.&lt;/p&gt;
&lt;p&gt;Pipeline&lt;br&gt;
A pipeline is the complete automated process that runs after a specific event, such as pushing code to a Git repository or opening a pull request. A pipeline can build your app, run tests, generate an APK, and even distribute it to testers — all without manual intervention.&lt;/p&gt;
&lt;p&gt;Think of a pipeline as a sequence of automated tasks executed in a predefined order.&lt;/p&gt;
&lt;p&gt;Workflow&lt;br&gt;
In GitHub Actions, a pipeline is defined using a workflow. A workflow is a YAML (.yml or .yaml) file stored inside the .github/workflows directory of your project.&lt;/p&gt;
&lt;p&gt;It describes what should happen, when it should happen, and how it should be executed.&lt;/p&gt;
&lt;p&gt;A repository can contain multiple workflows, each responsible for different tasks. For example, one workflow may run tests for every pull request, while another creates a release build whenever a new version is published.&lt;/p&gt;
&lt;p&gt;Event (Trigger)&lt;br&gt;
A workflow doesn’t run on its own — it needs an event, also known as a trigger.&lt;/p&gt;
&lt;p&gt;Common triggers include:&lt;/p&gt;
&lt;p&gt;Pushing code to a branch (push)&lt;br&gt;
Opening or updating a pull request (pull_request)&lt;br&gt;
Manually starting a workflow (workflow_dispatch)&lt;br&gt;
Creating a release (release)&lt;br&gt;
Whenever one of these events occurs, GitHub Actions starts the corresponding workflow.&lt;/p&gt;
&lt;p&gt;Runner&lt;br&gt;
A runner is the machine responsible for executing your workflow.&lt;/p&gt;
&lt;p&gt;GitHub provides hosted runners running Linux, Windows, and macOS. For Android projects, Linux runners are commonly used because they provide a fast and cost-effective environment for building Android applications.&lt;/p&gt;
&lt;p&gt;You can also configure your own self-hosted runner if you need custom hardware or software.&lt;/p&gt;
&lt;p&gt;Job&lt;br&gt;
A job is a collection of related tasks executed on the same runner.&lt;/p&gt;
&lt;p&gt;For example, one job might build the Android app, while another runs unit tests. Multiple jobs can run sequentially or in parallel, depending on how the workflow is configured.&lt;/p&gt;
&lt;p&gt;Step&lt;br&gt;
A step is a single task inside a job.&lt;/p&gt;
&lt;p&gt;Examples of steps include:&lt;/p&gt;
&lt;p&gt;Checking out the repository&lt;br&gt;
Setting up Java&lt;br&gt;
Installing Android dependencies&lt;br&gt;
Running Gradle commands&lt;br&gt;
Uploading the generated APK&lt;br&gt;
A job is simply a sequence of steps executed one after another.&lt;/p&gt;
&lt;p&gt;Action&lt;br&gt;
An Action is a reusable piece of automation that performs a specific task.&lt;/p&gt;
&lt;p&gt;Instead of writing everything from scratch, you can use actions created by GitHub or the community.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;actions/checkout downloads your repository onto the runner.&lt;br&gt;
actions/setup-java installs and configures Java.&lt;br&gt;
actions/upload-artifact uploads build outputs such as APKs or test reports.&lt;br&gt;
Actions make workflows shorter, cleaner, and easier to maintain.&lt;/p&gt;
&lt;p&gt;Artifact&lt;br&gt;
An artifact is a file generated during a workflow that you want to keep after the workflow finishes.&lt;/p&gt;
&lt;p&gt;In Android projects, common artifacts include:&lt;/p&gt;
&lt;p&gt;APK files&lt;br&gt;
Android App Bundles (AAB)&lt;br&gt;
Lint reports&lt;br&gt;
Unit test reports&lt;br&gt;
Code coverage reports&lt;br&gt;
Artifacts can be downloaded later for testing, debugging, or distribution.&lt;/p&gt;
&lt;p&gt;GitHub Actions&lt;br&gt;
Throughout this series, we’ll use GitHub Actions as our CI/CD platform.&lt;/p&gt;
&lt;p&gt;GitHub Actions is GitHub’s built-in automation service that allows you to create workflows using YAML files. It automatically responds to events in your repository, runs your workflows on runners, and reports the results all directly within GitHub.&lt;/p&gt;
&lt;p&gt;Now that you’re familiar with these core concepts, we’ll start creating our first Android CI workflow in the next part of this series.&lt;/p&gt;
&lt;p&gt;That all it for now…&lt;br&gt;
Lets stop here for now as this is already too much if your just starting out&lt;/p&gt;
&lt;p&gt;Now that you understand the purpose of CI/CD and the terminology behind it, it’s time to build your first pipeline.&lt;/p&gt;
&lt;p&gt;In Part 2, we’ll set up GitHub Actions, create our first workflow, and automatically build an Android application whenever code is pushed to GitHub.&lt;/p&gt;
&lt;p&gt;👋Bye!!!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28201/android-ci-cd-for-beginners-part-1-introduction</guid>
<pubDate>Tue, 22 Sep 2026 17:12:07 +0000</pubDate>
</item>
<item>
<title>Building High-Performance Text Editors in Kotlin Multiplatform with Krope</title>
<link>https://coderlegion.com/23397/building-high-performance-text-editors-in-kotlin-multiplatform-with-krope</link>
<description>&lt;p&gt;If you have ever tried building a text editor, an IDE, or a collaborative document tool, you quickly run into a fundamental wall: &lt;strong&gt;how do you store the text?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At first glance, a standard &lt;code&gt;String&lt;/code&gt; seems fine. But Strings are immutable; changing a single character in a 1MB file requires allocating a brand new 1MB array. So, you move to &lt;code&gt;StringBuilder&lt;/code&gt;. It’s mutable and fast for simple appends, but inserting or deleting text in the middle of a massive document still requires shifting hundreds of thousands of characters in memory — an O(N) operation that causes UI stuttering as your document grows.&lt;/p&gt;
&lt;p&gt;This is where the &lt;strong&gt;Rope&lt;/strong&gt; data structure comes in.&lt;/p&gt;
&lt;p&gt;Today, I want to introduce &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/krope&quot;&gt;&lt;strong&gt;Krope&lt;/strong&gt;&lt;/a&gt;, a high-performance, memory-safe, and reactive Rope library built entirely in Kotlin Multiplatform. Whether you are targeting JVM, Android, iOS, or WASM, Krope provides the engine you need to build next-generation text editing tools.&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-rope-&quot;&gt;What is a Rope?&lt;/h3&gt;
&lt;p&gt;Instead of storing text as a single massive, contiguous array of characters, a &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Rope_(data_structure)&quot;&gt;Rope&lt;/a&gt; stores text as a balanced binary tree (in Krope’s case, utilizing a Red-Black tree variant). The leaves of the tree contain short chunks of strings.&lt;/p&gt;
&lt;p&gt;When you insert text in the middle of a document, a Rope doesn’t copy the whole string. Instead, it splits a leaf node and inserts a new branch. This gives operations like insertion, deletion, and replacement a time complexity of O(log N).&lt;/p&gt;
&lt;p&gt;Furthermore, Krope implements &lt;strong&gt;structural sharing&lt;/strong&gt;. When a modification occurs, Krope doesn’t mutate the existing tree; it returns a &lt;em&gt;new&lt;/em&gt; tree that shares the untouched branches with the old one. This provides safe concurrent access, cheap undo/redo history.&lt;/p&gt;
&lt;h3 id=&quot;why-i-built-krope&quot;&gt;Why I Built Krope&lt;/h3&gt;
&lt;p&gt;The idea for Krope was born while building &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/haskcore&quot;&gt;haskcore&lt;/a&gt; — a full-featured IDE for Haskell written in Compose Desktop. It uses Skia for rendering, Tree-sitter for incremental syntax parsing, and the Language Server Protocol for code intelligence. It’s a real editor with 40+ modules, handling everything from syntax highlighting to code completion and project-wide diagnostics.&lt;/p&gt;
&lt;p&gt;Very quickly, I hit a wall: Kotlin’s standard &lt;code&gt;StringBuilder&lt;/code&gt; couldn't keep up. Editing large Haskell modules meant shifting massive text buffers on every keystroke, causing visible lag during syntax highlighting and LSP synchronization. I needed something better — and the rope data structure, battle-tested in editors like Emacs and Vim since the 90s, was the obvious answer.&lt;/p&gt;
&lt;p&gt;But Kotlin Multiplatform lacked a modern, reactive, and fully-featured rope implementation. So I built Krope to solve the specific pain points I encountered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cross-Platform Consistency: haskcore targets the desktop, but the same text engine should work on Android, iOS, and WASM for future ports or companion apps.&lt;/li&gt;
&lt;li&gt;Reactive by Design: Compose Multiplatform thrives on reactive state. Krope exposes every edit as a &lt;code&gt;SharedFlow&lt;/code&gt; event and every document state as a &lt;code&gt;StateFlow&lt;/code&gt;, making UI integration seamless.&lt;/li&gt;
&lt;li&gt;Byte-Level Encoding Awareness: LSP servers speak in byte offsets, not character positions. Krope natively tracks UTF-8, UTF-16, and UTF-32, giving you efficient access to byte coordinates backed by an LRU cache.&lt;/li&gt;
&lt;li&gt;Functional &amp;amp; Safe: Invalid operations shouldn’t crash your editor. Krope uses an &lt;code&gt;Either&amp;lt;Throwable, Result&amp;gt;&lt;/code&gt; pattern, so out-of-bounds inserts return errors you can handle gracefully — critical when processing remote edits from LSP or collaborative peers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;inside-the-krope-api&quot;&gt;Inside the Krope API&lt;/h3&gt;
&lt;p&gt;Krope separates the underlying data structure from the editor API. The primary interface you interact with is the &lt;code&gt;TextBuffer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is how simple it is to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;import io.github.numq.krope.core.Encoding
import io.github.numq.krope.text.*
import kotlinx.coroutines.launch

// 1. Initialize the buffer
val factory = RopeTextBufferFactory()
val buffer = factory.create(
    text = &quot;Hello World&quot;,
    encoding = Encoding.UTF8,
    lineEnding = TextLineEnding.LF,
    enablePooling = true // Enables internal string interning to save memory
).fold(
    onLeft = { error -&amp;gt; throw error },
    onRight = { it }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-reactive-paradigm&quot;&gt;The Reactive Paradigm&lt;/h3&gt;
&lt;p&gt;In a standard text editor, the UI needs to know exactly &lt;em&gt;what&lt;/em&gt; changed to update the screen or send a diff over the network for collaborative editing. Krope exposes two flows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;snapshot: StateFlow&amp;lt;TextSnapshot&amp;gt;&lt;/code&gt;: Represents the immutable current state of the document.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data: SharedFlow&amp;lt;TextEdit.Data&amp;gt;&lt;/code&gt;: Emits granular events whenever the text changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Observe all changes in real-time
launch {
    buffer.data.collect { editData -&amp;gt;
        when (editData) {
            is TextEdit.Data.Single.Insert -&amp;gt; 
                println(&quot;Inserted '${editData.insertedText}' at ${editData.startPosition}&quot;)
            is TextEdit.Data.Single.Delete -&amp;gt; 
                println(&quot;Deleted text ending at ${editData.oldEndPosition}&quot;)
            is TextEdit.Data.Single.Replace -&amp;gt; 
                println(&quot;Replaced '${editData.oldText}' with '${editData.newText}'&quot;)
            is TextEdit.Data.Batch -&amp;gt; 
                println(&quot;Batch processed ${editData.singles.size} operations atomically&quot;)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;line-and-column-awareness&quot;&gt;Line and Column Awareness&lt;/h3&gt;
&lt;p&gt;Most ropes just deal with raw character offsets. Krope natively understands &lt;code&gt;Lines&lt;/code&gt; and &lt;code&gt;Columns&lt;/code&gt; through &lt;code&gt;TextPosition&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// &quot;Hello Beautiful World&quot;
buffer.insert(TextPosition(line = 0, column = 5), &quot; Beautiful&quot;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Querying the snapshot is instantaneous thanks to cached tree node metadata
val snapshot = buffer.snapshot.value
println(&quot;Total Lines: ${snapshot.lines}&quot;)
println(&quot;Max Line Length: ${snapshot.maxLineLength}&quot;)

// Get byte offset for an LSP (Language Server Protocol)
val byteOffset = snapshot.getBytePosition(TextPosition(line = 0, column = 6))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;atomic-batch-operations&quot;&gt;Atomic Batch Operations&lt;/h3&gt;
&lt;p&gt;When you refactor code (like renaming a variable that appears 10 times in a file), you don’t want to trigger 10 separate UI redraws or clutter the undo stack. Krope supports &lt;code&gt;withBatch&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;buffer.withBatch { batch -&amp;gt;
    batch.replace(TextRange(TextPosition(0, 39), TextPosition(0, 42)), &quot;vatAmount&quot;)
    batch.replace(TextRange(TextPosition(1, 19), TextPosition(1, 22)), &quot;vatAmount&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This applies all edits internally, rebalances the tree &lt;em&gt;once&lt;/em&gt;, and emits a single &lt;code&gt;TextEdit.Data.Batch&lt;/code&gt; event to your listeners.&lt;/p&gt;
&lt;h3 id=&quot;performance-the-100-megabyte-test&quot;&gt;Performance: The 100 Megabyte Test&lt;/h3&gt;
&lt;p&gt;To answer the age-old question - &lt;em&gt;&quot;How does it perform with very large files?&quot;&lt;/em&gt; - I ran JMH benchmarks on the JVM comparing Krope to &lt;code&gt;StringBuilder&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We simulated the &quot;Real Editor Scenario&quot;: &lt;strong&gt;100 consecutive edits&lt;/strong&gt; (insertions) occurring right in the middle of a document. We scaled the document size from 100,000 characters all the way to &lt;strong&gt;100,000,000 characters (approximately 200 MB of text)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here are the results (in milliseconds per 100 operations):&lt;/p&gt;
&lt;p&gt;100,000 symbols&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Krope: &lt;strong&gt;~0.05 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;StringBuilder: &lt;strong&gt;~0.16 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Krope Advantage: &lt;strong&gt;~3x faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1,000,000 symbols&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Krope: &lt;strong&gt;~0.19 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;StringBuilder: &lt;strong&gt;~1.74 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Krope Advantage: &lt;strong&gt;~9x faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;10,000,000 symbols&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Krope: &lt;strong&gt;~2.07 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;StringBuilder: &lt;strong&gt;~17.86 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Krope Advantage: &lt;strong&gt;~8.5x faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;100,000,000 symbols&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Krope: &lt;strong&gt;~19.32 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;StringBuilder: &lt;strong&gt;~186.41 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Krope Advantage: &lt;strong&gt;~9.6x faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-does-stringbuilder-choke-&quot;&gt;Why does StringBuilder choke?&lt;/h3&gt;
&lt;p&gt;Look at how the &lt;code&gt;StringBuilder&lt;/code&gt; time multiplies by 10 every time the file size multiplies by 10. This is the curse of O(N) complexity. When you insert a single character in the middle of a 200MB &lt;code&gt;StringBuilder&lt;/code&gt;, Java has to use &lt;code&gt;System.arraycopy&lt;/code&gt; to physically shift 100MB of data in RAM to make room. Doing this 100 times forces the CPU to move &lt;strong&gt;10 gigabytes&lt;/strong&gt; of memory. This is exactly what causes UI thread stuttering and input lag in text editors.&lt;/p&gt;
&lt;h3 id=&quot;why-does-krope-win-&quot;&gt;Why does Krope win?&lt;/h3&gt;
&lt;p&gt;Because of &lt;strong&gt;structural sharing&lt;/strong&gt; and its Red-Black tree foundation. When making 100 edits, Krope doesn't copy 100MB of arrays. It simply splits a small leaf node, updates a few pointers up to the root, and does a lightning-fast O(log N) tree rebalance.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Note: In the benchmark above, the ~19.32 ms for Krope actually included the time to parse the 200MB string into a Rope tree from scratch on every iteration! The edits themselves are nearly instantaneous).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In a real-world typing or refactoring scenario, Krope maintains incredibly flat, predictable latency regardless of whether your file is 10 Kilobytes or 200 Megabytes, keeping your UI perfectly smooth while drastically reducing heap allocations.&lt;/p&gt;
&lt;h3 id=&quot;get-involved&quot;&gt;Get Involved&lt;/h3&gt;
&lt;p&gt;If you are building text-heavy applications in Compose Multiplatform, writing a code editor, or experimenting with CRDTs for collaborative editing, give Krope a try.&lt;/p&gt;
&lt;p&gt;The architecture is highly modular, separating the core data structures from the TextBuffer API, and includes built-in support for &lt;code&gt;kotlinx.serialization&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Check out the source code, play with the examples, and star the repository on GitHub!&lt;/p&gt;
&lt;p&gt;🔗 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/krope&quot;&gt;&lt;strong&gt;GitHub Repository: Krope&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23397/building-high-performance-text-editors-in-kotlin-multiplatform-with-krope</guid>
<pubDate>Fri, 24 Jul 2026 13:03:31 +0000</pubDate>
</item>
<item>
<title>Kotlin 2.4.0 Released</title>
<link>https://coderlegion.com/20530/kotlin-2-4-0-released</link>
<description>&lt;p&gt;JetBrains has released Kotlin 2.4.0. The update includes stable language features, standard library additions, Kotlin/Native changes for Swift interoperability, Kotlin/Wasm and Kotlin/JS improvements, and build tool updates for Gradle and Maven.&lt;/p&gt;
&lt;p&gt;The release is included in the latest versions of IntelliJ IDEA and Android Studio. Projects can update by setting the Kotlin version to 2.4.0 in their build scripts.&lt;/p&gt;
&lt;h3 id=&quot;language-and-standard-library&quot;&gt;Language and Standard Library&lt;/h3&gt;
&lt;p&gt;Several language features that were previously experimental are now stable, including context parameters, explicit backing fields, the @all meta-target for properties, and new defaulting rules for annotation use-site targets.&lt;/p&gt;
&lt;p&gt;The common standard library now has a stable kotlin.uuid.Uuid API. Kotlin 2.4.0 also adds sorted-order checks such as .isSorted() and .isSortedBy(), plus JVM extensions for converting unsigned integers to BigInteger.&lt;/p&gt;
&lt;p&gt;On Kotlin/JVM, the compiler can generate Java 26 bytecode, and annotations in Kotlin metadata are now enabled by default.&lt;/p&gt;
&lt;h3 id=&quot;kotlin-native-and-kmp&quot;&gt;Kotlin/Native and KMP&lt;/h3&gt;
&lt;p&gt;For Kotlin Multiplatform projects, the main changes are in Kotlin/Native. Swift export is now Alpha and has improved concurrency support: Kotlin suspend functions are exported as Swift async counterparts, and kotlinx.coroutines flows can be exported to Swift as AsyncSequence.&lt;/p&gt;
&lt;p&gt;KMP projects can also declare Swift packages as dependencies for iOS apps directly in Gradle configuration. JetBrains also provides migration support for projects that currently rely on CocoaPods dependencies.&lt;/p&gt;
&lt;p&gt;Kotlin/Native now supports Xcode 26.4, updates LLVM to version 21, and enables the concurrent mark and sweep garbage collector by default. The default minimum supported Apple target versions have also been raised to iOS and tvOS 15.0, macOS 12.0, and watchOS 8.0.&lt;/p&gt;
&lt;h3 id=&quot;wasm-js-and-build-tools&quot;&gt;Wasm, JS, and Build Tools&lt;/h3&gt;
&lt;p&gt;Kotlin/Wasm incremental compilation is now stable and enabled by default. Kotlin 2.4.0 also introduces experimental support for the WebAssembly Component Model.&lt;/p&gt;
&lt;p&gt;Kotlin/JS improves JavaScript and TypeScript export with support for value class export and ES2015 features when inlining JavaScript code.&lt;/p&gt;
&lt;p&gt;On the build side, Kotlin 2.4.0 is compatible with Gradle 9.5.0. Maven builds now support automatic alignment between Java and JVM target versions.&lt;/p&gt;
&lt;p&gt;Official announcement:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.jetbrains.com/kotlin/2026/06/kotlin-2-4-0-released/&quot;&gt;https://blog.jetbrains.com/kotlin/2026/06/kotlin-2-4-0-released/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Detailed release notes:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/whatsnew24.html&quot;&gt;https://kotlinlang.org/docs/whatsnew24.html&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20530/kotlin-2-4-0-released</guid>
<pubDate>Sat, 13 Jun 2026 19:31:15 +0000</pubDate>
</item>
<item>
<title>Kotlin Professional Certificate by JetBrains</title>
<link>https://coderlegion.com/18439/kotlin-professional-certificate-by-jetbrains</link>
<description>&lt;p&gt;JetBrains has launched the Kotlin Professional Certificate by JetBrains, a new LinkedIn Learning path designed to help developers build practical Kotlin and Kotlin Multiplatform skills.&lt;/p&gt;
&lt;p&gt;The program is structured around four courses and takes developers from Kotlin fundamentals through object-oriented and asynchronous programming, Kotlin Multiplatform development, and full-stack app development with Ktor and Compose Multiplatform. JetBrains says the certificate is aimed at developers with basic programming knowledge who want to learn Kotlin and explore how it can be used across Android, iOS, web, desktop, and backend environments.&lt;/p&gt;
&lt;p&gt;The course includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kotlin Essential Training&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kotlin Multiplatform&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ktor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compose&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After completing the course and passing the final exam, developers can earn a certificate that can be downloaded, shared, and added to a LinkedIn profile.&lt;/p&gt;
&lt;p&gt;Additional info at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.jetbrains.com/kotlin/2026/04/kotlin-professional-certificate-by-jetbrains-now-on-linkedin-learning/&quot;&gt;https://blog.jetbrains.com/kotlin/2026/04/kotlin-professional-certificate-by-jetbrains-now-on-linkedin-learning/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Link to the course:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/learning/paths/kotlin-professional-certificate-by-jetbrains&quot;&gt;https://www.linkedin.com/learning/paths/kotlin-professional-certificate-by-jetbrains&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18439/kotlin-professional-certificate-by-jetbrains</guid>
<pubDate>Wed, 20 May 2026 10:00:04 +0000</pubDate>
</item>
<item>
<title>JetBrains Just Changed KMP Structure. Here's What They Didn't Tell You.</title>
<link>https://coderlegion.com/18238/jetbrains-just-changed-kmp-structure-heres-what-they-didnt-tell-you</link>
<description>&lt;p&gt;JetBrains &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.jetbrains.com/kotlin/2026/05/new-kmp-default-structure/&quot;&gt;announced&lt;/a&gt; a new default project structure for KMP. Two shared modules — &lt;code&gt;sharedLogic&lt;/code&gt; and &lt;code&gt;sharedUI&lt;/code&gt; — plus separate application modules for each platform. Clean. Simple. Obvious.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17995218555422079821&quot; alt=&quot;New KMP template&quot;&gt;&lt;/p&gt;
&lt;p&gt;It's also a trap.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-sharedlogic-and-sharedui&quot;&gt;The problem with &lt;code&gt;sharedLogic&lt;/code&gt; and &lt;code&gt;sharedUI&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A fresh &lt;code&gt;sharedLogic&lt;/code&gt; module looks innocent. Business logic, models, repositories. &lt;code&gt;sharedUI&lt;/code&gt; holds your Composables. What could go wrong?&lt;/p&gt;
&lt;p&gt;Three months later &lt;code&gt;sharedLogic&lt;/code&gt; contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Network clients&lt;/li&gt;
&lt;li&gt;Database models&lt;/li&gt;
&lt;li&gt;Constants&lt;/li&gt;
&lt;li&gt;Utility functions&lt;/li&gt;
&lt;li&gt;Something called &lt;code&gt;CommonUtils.kt&lt;/code&gt; that nobody understands but everyone is afraid to delete&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;sharedLogic&lt;/code&gt; has no internal boundaries. Everything depends on everything. You can import a database model from a use case and nobody stops you. The compiler certainly won't.&lt;/p&gt;
&lt;p&gt;JetBrains tells you where to start. They don't tell you what happens when two modules become 50,000 lines.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-happens&quot;&gt;What actually happens&lt;/h3&gt;
&lt;p&gt;You try to split &lt;code&gt;sharedLogic&lt;/code&gt; into more modules. But there are no rules. So you end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:sharedLogic:core&lt;/code&gt; — the new dumping ground&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:sharedLogic:data&lt;/code&gt; — which also contains business logic somehow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dependencies look like spaghetti. Changing one module breaks three others. You didn't enforce boundaries early, and now every refactoring is a gamble.&lt;/p&gt;
&lt;h3 id=&quot;flip-the-next-step-after-sharedlogic&quot;&gt;FLIP: the next step after &lt;code&gt;sharedLogic&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/flip&quot;&gt;FLIP&lt;/a&gt; is an architectural pattern that gives you rules for splitting &lt;code&gt;sharedLogic&lt;/code&gt; before it becomes a problem.&lt;/p&gt;
&lt;p&gt;Five layers. One direction. Zero horizontal dependencies.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Purpose &lt;/th&gt;&lt;th&gt;Depends on&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;:common:core &lt;/td&gt;&lt;td&gt;Foundation &lt;/td&gt;&lt;td&gt;Nothing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;:service &lt;/td&gt;&lt;td&gt;Domain logic without UI &lt;/td&gt;&lt;td&gt;&lt;code&gt;:common:core&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;:feature &lt;/td&gt;&lt;td&gt;UI features &lt;/td&gt;&lt;td&gt;&lt;code&gt;:service&lt;/code&gt;, &lt;code&gt;:common:core&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;:entrypoint &lt;/td&gt;&lt;td&gt;DI assembly + slot composition &lt;/td&gt;&lt;td&gt;Everything above&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;:platform &lt;/td&gt;&lt;td&gt;Platform entry point &lt;/td&gt;&lt;td&gt;&lt;code&gt;:entrypoint&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The rule is simple: &lt;strong&gt;features don't know about other features. services don't know about other services.&lt;/strong&gt; Dependencies only flow down.&lt;/p&gt;
&lt;p&gt;Your IDE enforces this. You literally cannot import a feature from another feature — Gradle won't compile. The structure prevents the dumpster fire before it starts.&lt;/p&gt;
&lt;h3 id=&quot;start-with-jetbrains-grow-with-flip-&quot;&gt;Start with JetBrains. Grow with FLIP.&lt;/h3&gt;
&lt;p&gt;The new KMP structure is a great starting point. Two modules is better than one. But when &lt;code&gt;sharedLogic&lt;/code&gt; starts feeling like a junk drawer, you know what to do.&lt;/p&gt;
&lt;p&gt;FLIP is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/numq/flip&quot;&gt;github.com/numq/flip&lt;/a&gt;. Template included. Konsist tests included. Rules over conventions.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18238/jetbrains-just-changed-kmp-structure-heres-what-they-didnt-tell-you</guid>
<pubDate>Mon, 18 May 2026 18:26:05 +0000</pubDate>
</item>
<item>
<title>FLIP: Modular Architecture for KMP</title>
<link>https://coderlegion.com/18126/flip-modular-architecture-for-kmp</link>
<description>&lt;p&gt;While building &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.com/numq/haskcore&quot;&gt;haskcore&lt;/a&gt;&lt;/strong&gt; - an IDE for Haskell on Compose Desktop - I extracted rules that allowed me to create an architecture of 40 isolated modules, where each module knows only what it needs.&lt;/p&gt;
&lt;p&gt;That's how &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.com/numq/flip&quot;&gt;FLIP - Feature-Layered Isolated Platform&lt;/a&gt;&lt;/strong&gt; - was born.&lt;/p&gt;
&lt;p&gt;The main problem I faced during development: &lt;strong&gt;horizontal dependencies make the system fragile&lt;/strong&gt;. The solution - strict vertical hierarchy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1320007955686771447&quot; alt=&quot;FLIP layers&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FLIP&lt;/strong&gt; consists of the following isolated modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:common:core&lt;/code&gt; - common logic components.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:common:presentation&lt;/code&gt; - common UI components.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:service&lt;/code&gt; - UI-less domain module. Provides a service interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;interface SeedService : AutoCloseable {
    val seed: Flow&amp;lt;Seed&amp;gt;
    suspend fun generateSeed(): Either&amp;lt;Throwable, Unit&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:feature:core&lt;/code&gt; — logic module of a UI-feature. Knows nothing about other features. Uses services from &lt;code&gt;:service&lt;/code&gt;. Provides use cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class GenerateRemoteRandomSeed(private val seedService: SeedService) : UseCase.Action {
    override suspend fun Raise&amp;lt;Throwable&amp;gt;.action() = seedService.generateSeed().bind()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:feature:presentation&lt;/code&gt; — UI module of a UI-feature. Knows only about its &lt;code&gt;:feature:core&lt;/code&gt; and uses its use cases. Provides the view. Knows only about its own &lt;code&gt;:feature:core&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:entrypoint&lt;/code&gt; — module where dependency configuration and composition of views from &lt;code&gt;:feature:presentation&lt;/code&gt; into a single Composable happens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:platform&lt;/code&gt; — module that initializes the Composable provided by &lt;code&gt;:entrypoint&lt;/code&gt; for each platform.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9437479124804445323&quot; alt=&quot;FLIP dependencies&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Arrows show the direction of dependencies.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-to-structure-your-system-&quot;&gt;How to structure your system?&lt;/h3&gt;
&lt;p&gt;Decide which features your app should have, then determine whether each feature has a UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If it does — it’s a &lt;strong&gt;feature&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If it doesn’t — it’s a &lt;strong&gt;service&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If one feature’s logic is needed by another — extract it into a separate &lt;strong&gt;service&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Important: both &lt;code&gt;:service&lt;/code&gt; and &lt;code&gt;:feature:core&lt;/code&gt; contain domain logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:service&lt;/code&gt; — core domain.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:feature:core&lt;/code&gt; — presentation domain (use cases).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Wrong:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:service:foo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:feature:foo:core&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:feature:foo:presentation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:service:foo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:feature:bar:core&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:feature:bar:presentation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Navigation in &lt;strong&gt;FLIP&lt;/strong&gt; is just another feature. It aggregates and routes views passed as lambda slots, keeping it independent from other features.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;NavigationView(
    splash = {
        SplashView(applicationScope = applicationScope)
    },
    profile = {
        ProfileView(applicationScope = applicationScope)
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach lets you add new screens without touching existing features.&lt;/p&gt;
&lt;p&gt;A complete example is available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.com/numq/flip&quot;&gt;official GitHub repository&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18126/flip-modular-architecture-for-kmp</guid>
<pubDate>Sun, 17 May 2026 04:49:14 +0000</pubDate>
</item>
<item>
<title>A recipe for the Perfect Use Case</title>
<link>https://coderlegion.com/18129/a-recipe-for-the-perfect-use-case</link>
<description>&lt;p&gt;Today we’re cooking the perfect Use Case — and &lt;strong&gt;CQRS&lt;/strong&gt; will be our recipe.&lt;/p&gt;
&lt;h3 id=&quot;ingredients&quot;&gt;Ingredients&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt; — fire and forget (logout, clear cache)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query&lt;/strong&gt; — read data (list products)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command&lt;/strong&gt; — write data (update profile)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exchange&lt;/strong&gt; — transform data (login)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All four types extend a sealed interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface UseCase&amp;lt;Input, Output&amp;gt; {
    interface Action : UseCase&amp;lt;Unit, Unit&amp;gt;
    interface Query&amp;lt;Output&amp;gt; : UseCase&amp;lt;Unit, Output&amp;gt;
    interface Command&amp;lt;Input&amp;gt; : UseCase&amp;lt;Input, Unit&amp;gt;
    interface Exchange&amp;lt;Input, Output&amp;gt; : UseCase&amp;lt;Input, Output&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17753012738574751457&quot; alt=&quot;Usecases&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;three-ways-to-season-it&quot;&gt;Three Ways to Season It&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Arrow (Typed Errors)&lt;/strong&gt; — the chef’s choice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class GenerateSeed(
    private val seedService: SeedService
) : UseCase.Action {
    override suspend fun Raise&amp;lt;Throwable&amp;gt;.action() =
        seedService.generateSeed().bind()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result (Standard Wrapper)&lt;/strong&gt; — zero dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class GenerateSeed(private val service: SeedService) : UseCase.Action {
    override suspend fun action() = service.generateSeed().getOrThrow()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Raw (Direct Execution)&lt;/strong&gt; — zero overhead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class GenerateSeed(private val service: SeedService) : UseCase.Action {
    override suspend fun action() = service.generateSeed()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-this-recipe-works&quot;&gt;Why This Recipe Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;No more &lt;code&gt;UseCase&amp;lt;Unit, Unit&amp;gt;&lt;/code&gt; noise&lt;/li&gt;
&lt;li&gt;Every use case follows the same structure&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Query&lt;/code&gt; or &lt;code&gt;Command&lt;/code&gt; makes intent obvious&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Full implementation in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.com/numq/usecase&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18129/a-recipe-for-the-perfect-use-case</guid>
<pubDate>Sun, 17 May 2026 04:47:50 +0000</pubDate>
</item>
<item>
<title>Stop Fighting Your State. Reduce And Conquer It.</title>
<link>https://coderlegion.com/18130/stop-fighting-your-state-reduce-and-conquer-it</link>
<description>&lt;p&gt;&lt;strong&gt;MVVM&lt;/strong&gt; seemed simple. Then you added ten MutableStateFlow properties to your ViewModel. &lt;strong&gt;MVI&lt;/strong&gt; promised purity. Then you wrote a middleware for side effects.&lt;/p&gt;
&lt;p&gt;There’s a better way.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-mvvm&quot;&gt;The Problem With MVVM&lt;/h2&gt;
&lt;p&gt;A typical ViewModel looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ProfileViewModel : ViewModel() {
    val name = MutableStateFlow(&quot;&quot;)
    val email = MutableStateFlow(&quot;&quot;)
    val isLoading = MutableStateFlow(false)
    val error = MutableStateFlow&amp;lt;String?&amp;gt;(null)
    fun updateProfile(name: String) {
        viewModelScope.launch {
            isLoading.value = true
            try {
                profileService.update(name)
                *Emails are not allowed*e = name
            } catch (e: Exception) {
                error.value = e.message
            } finally {
                isLoading.value = false
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Five mutable properties. Loading state scattered across three places. Error handling duplicated in every method. And the ViewModel doesn’t own its side effects — &lt;code&gt;viewModelScope&lt;/code&gt; does.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-mvi&quot;&gt;The Problem With MVI&lt;/h2&gt;
&lt;p&gt;MVI fixes the state explosion by putting everything in a sealed interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface ProfileState {
    data object Loading : ProfileState
    data class Loaded(val name: String, val email: String) : ProfileState
    data class Error(val message: String) : ProfileState
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But MVI doesn’t tell you how to handle side effects. Some use middleware. Some use &lt;code&gt;Channel&lt;/code&gt;. Some hack it with &lt;code&gt;LaunchedEffect&lt;/code&gt;. Every project reinvents the wheel.&lt;/p&gt;
&lt;h2 id=&quot;reduce-amp-conquer&quot;&gt;Reduce &amp;amp; Conquer&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2820081858493314974&quot; alt=&quot;Reduce &amp;amp; Conquer layers&quot;&gt;&lt;/p&gt;
&lt;p&gt;The core idea: &lt;em&gt;a reducer is a pure function that returns more than just state&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class Transition&amp;lt;State, Event&amp;gt;(
    val state: State,
    val events: List&amp;lt;Event&amp;gt; = emptyList(),
    val effects: List&amp;lt;Effect&amp;gt; = emptyList()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A transition has three outputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt; - the new state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt; - one-shot notifications (navigation, snackbar)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effects&lt;/strong&gt; - long-running or async work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Effects are first-class citizens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface Effect {
    data class Stream&amp;lt;Command&amp;gt;(
        val key: Any,
        val flow: Flow&amp;lt;Command&amp;gt;,
        val strategy: Strategy = Strategy.Sequential,
        val fallback: (suspend (Throwable) -&amp;gt; Command)? = null
    ) : Effect
    data class Action&amp;lt;Command&amp;gt;(
        val key: Any,
        val fallback: (suspend (Throwable) -&amp;gt; Command)? = null,
        val block: suspend () -&amp;gt; Command
    ) : Effect
    data class Cancel(val key: Any) : Effect
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stream&lt;/strong&gt; - subscribes to a flow, emits commands back&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Action&lt;/strong&gt; - runs one async operation, emits a command&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cancel&lt;/strong&gt; - cancels by key, preventing leaks&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-reducer-in-practice&quot;&gt;A Reducer in Practice&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ProfileReducer(
    private val profileService: ProfileService
) : Reducer&amp;lt;ProfileState, ProfileCommand, ProfileEvent&amp;gt; {
    override fun reduce(
        state: ProfileState,
        command: ProfileCommand
    ): Transition&amp;lt;ProfileState, ProfileEvent&amp;gt; = when (command) {
        is ProfileCommand.UpdateProfile -&amp;gt; transition(
            state.copy(isLoading = true)
        ).effect(
            action(
                key = &quot;update_profile&quot;,
                fallback = { ProfileCommand.ProfileError(it) },
                block = {
                    profileService.update(command.name)
                    ProfileCommand.ProfileUpdated
                }
            )
        )
        is ProfileCommand.ProfileUpdated -&amp;gt; transition(
            state.copy(isLoading = false)
        ).event(ProfileEvent.NavigateBack)
        is ProfileCommand.ProfileError -&amp;gt; transition(
            state.copy(
                isLoading = false,
                error = command.throwable.message
            )
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;viewModelScope&lt;/code&gt;. No &lt;code&gt;LaunchedEffect&lt;/code&gt;. No mutable properties. One pure function.&lt;/p&gt;
&lt;h2 id=&quot;why-this-is-the-benchmark&quot;&gt;Why This Is The Benchmark&lt;/h2&gt;
&lt;h3 id=&quot;mvvm&quot;&gt;MVVM&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Multiple MutableStateFlow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: viewModelScope.launch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Manual&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Mock ViewModel&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;mvi&quot;&gt;MVI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Single sealed class/interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: Ad-hoc (middleware, Channel)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Manual&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Mock reducer + middleware&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;reduce-amp-conquer-1&quot;&gt;Reduce &amp;amp; Conquer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Single sealed interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: Built-in (Effect)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Automatic by key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: One pure function call&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing a reducer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Test
fun `update profile sets loading and fires effect`() {
    val transition = reducer.reduce(
        state = ProfileState(),
        command = ProfileCommand.UpdateProfile(&quot;Alice&quot;)
    )
    assertTrue(transition.state.isLoading)
    assertEquals(1, transition.effects.size)
    assertEquals(0, transition.events.size)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One call. No coroutines. No mocks.&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The Rule&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;State flows down. Commands flow up. Effects manage the rest.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The View sends Commands. The Reducer returns a Transition with new State, Events, and Effects. The Feature executes Effects and feeds resulting Commands back into the Reducer. That's it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16113002570422559213&quot; alt=&quot;Reduce &amp;amp; Conquer diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;Full implementation in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.numq/reduce-and-conquer&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18130/stop-fighting-your-state-reduce-and-conquer-it</guid>
<pubDate>Sun, 17 May 2026 04:47:30 +0000</pubDate>
</item>
<item>
<title>Contract Testing in Kotlin</title>
<link>https://coderlegion.com/15398/contract-testing-in-kotlin</link>
<description>&lt;h1&gt;Contract Testing in Kotlin: Kill &quot;Works on My Machine&quot; API Failures&lt;/h1&gt;
&lt;p&gt;Frontend deploys. Backend deploys. Everything looks green in CI, yet production is screaming with 500s because a field was renamed and someone’s Swagger doc was three sprints behind. We've all been there—cutting emergency hotfixes while the PM asks for a post-mortem you don't have time to write.&lt;/p&gt;
&lt;p&gt;The root cause? &lt;strong&gt;API Drift.&lt;/strong&gt; One team tweaked a schema, the other team’s code still expects the old one. In a monolith, this is a compiler error. In microservices, it’s a production outage. As a Kotlin dev, I’m done with manual coordination. Contract testing makes these failures structurally impossible by verifying interfaces &lt;em&gt;before&lt;/em&gt; they hit the pipeline.&lt;/p&gt;
&lt;div style=&quot;background-color: #f0ebff; border-left: 4px solid #7f52ff; padding: 20px; margin: 25px 0; border-radius: 6px;&quot;&gt;
    &lt;h2 style=&quot;color: #7f52ff; font-size: 1.4rem; margin-top: 0;&quot;&gt;The No-Nonsense Takeaway&lt;/h2&gt;
    &lt;ul style=&quot;margin-bottom: 0;&quot;&gt;
        &lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Contract tests run in &amp;lt;60s. No shared environments, no flaky network, just raw build-time validation.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;The Stack:&lt;/strong&gt; &lt;code&gt;Pact&lt;/code&gt; for consumer-driven polyglot setups or &lt;code&gt;Spring Cloud Contract&lt;/code&gt; for tight-knit Spring monorepos.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;The Gatekeeper:&lt;/strong&gt; &lt;code&gt;can-i-deploy&lt;/code&gt; is your CI/CD enforcer. If the contract is broken, the deployment stops. &lt;code&gt;exit code 1&lt;/code&gt;. No excuses.&lt;/li&gt;
    &lt;/ul&gt;
&lt;/div&gt;
&lt;h2&gt;Why Integration Tests Fail You&lt;/h2&gt;
&lt;p&gt;Integration tests check if two services play nice &lt;em&gt;now&lt;/em&gt;. But services deploy independently. By the time your consumer merges, the provider might have moved from v1.2 to v1.4. Your integration test was green for a version pair that no longer exists in any environment. Contract testing closes this gap by ensuring independent verification against a machine-readable artifact.&lt;/p&gt;
&lt;h2&gt;Kotlin Implementation&lt;/h2&gt;
&lt;p&gt;Kotlin’s DSL-friendly syntax makes contract definitions look like actual specs, not just more boilerplate. Here’s how you get started with the dependencies:&lt;/p&gt;
&lt;pre style=&quot;background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; font-family: monospace;&quot;&gt;
// build.gradle.kts
dependencies {
    testImplementation(&quot;au.com.dius.pact.consumer:junit5:4.6.5&quot;)
    testImplementation(&quot;io.mockk:mockk:1.13.8&quot;)
    testImplementation(&quot;io.rest-assured:kotlin-extensions:5.4.0&quot;)
}
&lt;/pre&gt;
&lt;p&gt;In the full breakdown, I dive into why &lt;code&gt;stringType&lt;/code&gt; matchers are superior to hardcoded values (avoiding brittle tests) and how to use a &lt;strong&gt;Pact Broker&lt;/strong&gt; to see exactly which fields are safe to delete without breaking your consumers.&lt;/p&gt;
&lt;h2&gt;When is it overkill?&lt;/h2&gt;
&lt;p&gt;If you're in a single-team monorepo where everything deploys in lockstep, don't over-engineer it. But the second you have independent teams and separate deployment cycles, contract testing isn't a luxury—it's your only way to guarantee the system doesn't shatter on every merge.&lt;/p&gt;
&lt;hr style=&quot;border: 0; border-top: 2px solid #eee; margin: 40px 0 20px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Krun Dev Code&lt;/strong&gt;&lt;br&gt;
&lt;span style=&quot;color: #7f8c8d; font-style: italic;&quot;&gt;Stop guessing. Start verifying.&lt;/span&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15398/contract-testing-in-kotlin</guid>
<pubDate>Tue, 21 Apr 2026 14:13:28 +0000</pubDate>
</item>
<item>
<title>Understanding Flutter Method Channels Through a Real Production Bug</title>
<link>https://coderlegion.com/14995/understanding-flutter-method-channels-through-a-real-production-bug</link>
<description>&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Flutter is amazing—until it isn’t.&lt;/p&gt;
&lt;p&gt;Most of the time, you can build entire apps without ever touching native code. But the moment you hit a platform limitation, things get… interesting.&lt;/p&gt;
&lt;p&gt;This blog isn’t just about Method Channels.&lt;/p&gt;
&lt;p&gt;It’s about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When Flutter abstractions break down&lt;/li&gt;
&lt;li&gt;How to take control using native Android&lt;/li&gt;
&lt;li&gt;And how to think about Flutter ↔ Native communication like a system, not a hack&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Funny enough, this blog comes from a real bug I hit while building my app &lt;em&gt;Anahad&lt;/em&gt; — a saadhna app that plays mantra chants.&lt;/p&gt;
&lt;p&gt;And yeah… it broke in a very weird way.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-problem-why-this-blog-exists-&quot;&gt;The Real Problem (Why This Blog Exists)&lt;/h3&gt;
&lt;p&gt;I was building a saadhna app that plays mantra chants on loop.&lt;/p&gt;
&lt;p&gt;Simple requirement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Play audio  &lt;/li&gt;
&lt;li&gt;Loop it continuously  &lt;/li&gt;
&lt;li&gt;Run in the background (screen off)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;audio_service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;just_audio&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;just_audio_background&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything worked perfectly…&lt;/p&gt;
&lt;p&gt;Until it didn’t.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-bug&quot;&gt;The Bug&lt;/h4&gt;
&lt;p&gt;After ~940–960 loops on Android (specifically Android 12+):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Playback would stop silently  &lt;/li&gt;
&lt;li&gt;No crash  &lt;/li&gt;
&lt;li&gt;No exception  &lt;/li&gt;
&lt;li&gt;Only when the screen was off  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disabling battery optimization  &lt;/li&gt;
&lt;li&gt;Proper background setup  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Still broke.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-weird-part&quot;&gt;The Weird Part&lt;/h4&gt;
&lt;p&gt;It &lt;em&gt;consistently&lt;/em&gt; failed around the same loop range (~940–960).&lt;/p&gt;
&lt;p&gt;Not 100. Not 500. Not random.&lt;/p&gt;
&lt;p&gt;That kind of consistency usually means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Something deeper is going on — not just app-level logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;at-this-point-two-choices&quot;&gt;At This Point, Two Choices&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Keep debugging blindly inside Flutter  &lt;/li&gt;
&lt;li&gt;Drop down to native Android and take control  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I chose option 2, cz i never interacted with native code and wanted to explore it&lt;/p&gt;
&lt;p&gt;Fun part, i had an android 11 device which wasn't having this bug at all, tested on other android 11 devices as well&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-flutter-isn-t-always-enough&quot;&gt;Why Flutter Isn’t Always Enough&lt;/h3&gt;
&lt;p&gt;Flutter is great, but it abstracts away platform details.&lt;/p&gt;
&lt;p&gt;That’s usually good—until you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deep lifecycle control  &lt;/li&gt;
&lt;li&gt;System-level integrations  &lt;/li&gt;
&lt;li&gt;Debug visibility into platform behavior  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Common cases where native is needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Background services  &lt;/li&gt;
&lt;li&gt;Media playback control  &lt;/li&gt;
&lt;li&gt;Hardware interaction  &lt;/li&gt;
&lt;li&gt;Platform-specific APIs  &lt;/li&gt;
&lt;li&gt;Performance-critical logic  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my case:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I needed more control over how Android handles long-running audio playback. Just to be absolutely sure that it was not the issue of the package that i was using&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-method-channels-the-right-mental-model-&quot;&gt;Understanding Method Channels (The Right Mental Model)&lt;/h3&gt;
&lt;p&gt;Before code, get this clear:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Dart is the client. Native Android is the server.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When you write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;platform.invokeMethod(&quot;play&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’re essentially doing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dart → sends request&lt;/li&gt;
&lt;li&gt;Android → receives it&lt;/li&gt;
&lt;li&gt;Executes native logic&lt;/li&gt;
&lt;li&gt;Returns response&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it like:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;An internal API call inside your app&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No HTTP. No network. Just a binary bridge.&lt;/p&gt;
&lt;p&gt;Once you understand this, Method Channels stop feeling “magical”.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Flutter (Dart)
├── UI Layer
├── Service Layer (PlaybackService.dart)
└── State Management

        ↓↓↓ Method Channel ↓↓↓

Android (Kotlin)
├── MethodChannel Handler
├── PlaybackService (MediaSessionService)
└── ExoPlayer
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;flow-step-by-step-&quot;&gt;Flow (Step-by-Step)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;User taps &quot;Play&quot;&lt;/li&gt;
&lt;li&gt;Dart calls &lt;code&gt;invokeMethod(&quot;play&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MethodChannel sends message&lt;/li&gt;
&lt;li&gt;Kotlin receives it&lt;/li&gt;
&lt;li&gt;Native service executes logic&lt;/li&gt;
&lt;li&gt;Result is sent back&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-native-side-android-kotlin-&quot;&gt;The Native Side: Android (Kotlin)&lt;/h3&gt;
&lt;h4 id=&quot;why-mediasessionservice-&quot;&gt;Why MediaSessionService?&lt;/h4&gt;
&lt;p&gt;Instead of a basic service, we use:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;MediaSessionService&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integrates with system controls (lock screen, notifications)&lt;/li&gt;
&lt;li&gt;Handles background playback more reliably&lt;/li&gt;
&lt;li&gt;Works with audio focus and lifecycle events&lt;/li&gt;
&lt;li&gt;Plays nicely with the Android system&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;playbackservice-kt&quot;&gt;PlaybackService.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class PlaybackService : MediaSessionService() {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()

        val audioAttributes =
            AudioAttributes.Builder()
                .setUsage(C.USAGE_MEDIA)
                .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
                .build()

        val player =
            ExoPlayer.Builder(this)
                .setAudioAttributes(audioAttributes, true)
                .setHandleAudioBecomingNoisy(true)
                .setWakeMode(C.WAKE_MODE_NETWORK)
                .build()

        player.addListener(object : Player.Listener {
            override fun onPlayerError(error: PlaybackException) {
                Log.e(&quot;PlaybackService&quot;, &quot;Error: ${error.message}&quot;, error)
            }
        })

        val callback = object : MediaSession.Callback {}

        mediaSession = MediaSession.Builder(this, player)
            .setCallback(callback)
            .build()
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-flutter-side-dart&quot;&gt;The Flutter Side: Dart&lt;/h3&gt;
&lt;h4 id=&quot;service-wrapper&quot;&gt;Service Wrapper&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class PlaybackService {
  static const platform =
      MethodChannel('com.theahambrahmasmi.anahad/playback');

  static Future&amp;lt;void&amp;gt; play(String url) async {
    await platform.invokeMethod('play', {'url': url});
  }

  static Future&amp;lt;void&amp;gt; pause() async {
    await platform.invokeMethod('pause');
  }

  static Future&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; getStatus() async {
    final result = await platform.invokeMethod('getStatus');
    return Map&amp;lt;String, dynamic&amp;gt;.from(result);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;kotlin-method-channel-handler&quot;&gt;Kotlin: Method Channel Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class PlaybackMethodHandler(private val context: Context) {

    fun setupChannel(flutterEngine: FlutterEngine) {
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            &quot;com.theahambrahmasmi.anahad/playback&quot;
        ).setMethodCallHandler { call, result -&amp;gt;

            when (call.method) {
                &quot;play&quot; -&amp;gt; {
                    val url = call.argument&amp;lt;String&amp;gt;(&quot;url&quot;)
                    val intent = Intent(context, PlaybackService::class.java)
                    intent.putExtra(&quot;audio_url&quot;, url)
                    context.startService(intent)
                    result.success(null)
                }

                &quot;pause&quot; -&amp;gt; {
                    val intent = Intent(context, PlaybackService::class.java)
                    context.stopService(intent)
                    result.success(null)
                }

                else -&amp;gt; result.notImplemented()
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-insight-the-looping-problem&quot;&gt;Real-World Insight: The Looping Problem&lt;/h3&gt;
&lt;p&gt;If you're building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Meditation apps&lt;/li&gt;
&lt;li&gt;Chanting apps&lt;/li&gt;
&lt;li&gt;Ambient loop apps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Be careful.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Android may silently stop long-running playback after extended loops.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Battery optimization disabled&lt;/li&gt;
&lt;li&gt;Background services configured&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Test aggressively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;500+ loops&lt;/li&gt;
&lt;li&gt;Screen-off playback&lt;/li&gt;
&lt;li&gt;Different OEM devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not just Flutter—it’s often &lt;strong&gt;Android system behavior&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-about-the-root-cause-&quot;&gt;What About The Root Cause?&lt;/h3&gt;
&lt;p&gt;Short answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I don’t fully know yet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And yeah — that’s intentional honesty.&lt;/p&gt;
&lt;p&gt;What I &lt;em&gt;do&lt;/em&gt; know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It only happens on &lt;strong&gt;Android 12+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It triggers after long-running background playback&lt;/li&gt;
&lt;li&gt;It happens silently (no crash, no logs)&lt;/li&gt;
&lt;li&gt;It correlates with a high loop count (~950)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;current-hypotheses-&quot;&gt;Current Hypotheses:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Doze / background execution limits&lt;/li&gt;
&lt;li&gt;Audio focus or media session interruption&lt;/li&gt;
&lt;li&gt;ExoPlayer internal behavior after prolonged looping&lt;/li&gt;
&lt;li&gt;OEM-level process killing&lt;/li&gt;
&lt;li&gt;Resource exhaustion over time&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;we-ll-be-updating-this&quot;&gt;We’ll Be Updating This&lt;/h3&gt;
&lt;p&gt;Once I dig deeper and find the exact root cause, I’ll update this blog with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What exactly broke&lt;/li&gt;
&lt;li&gt;Why Android 12+ behaves this way&lt;/li&gt;
&lt;li&gt;The proper fix (not just experimental ones)&lt;/li&gt;
&lt;li&gt;What you should do in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because honestly?&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Debugging something that breaks at the OS level is messy… and kinda fun.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;I eventually moved back to &lt;code&gt;just_audio&lt;/code&gt; because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The problematic flow was no longer needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So yeah…&lt;/p&gt;
&lt;p&gt;This experimental fix wasn’t used in production.&lt;/p&gt;
&lt;p&gt;But it gave me something way more valuable:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A deeper understanding of how Flutter actually works under the hood.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And honestly?&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I’m not perfect. I didn’t fully solve it (yet).&lt;br&gt;
But now I know one more thing than I did before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that’s what matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Flutter is great, but not enough for everything&lt;/li&gt;
&lt;li&gt;Method Channels = internal RPC bridge&lt;/li&gt;
&lt;li&gt;Native gives you control Flutter abstracts away&lt;/li&gt;
&lt;li&gt;Android background behavior can be unpredictable&lt;/li&gt;
&lt;li&gt;Sometimes… you won’t have all the answers immediately&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;If you're building something serious:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Don’t just rely on abstractions. Understand what’s underneath.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s where real engineering begins.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/14995/understanding-flutter-method-channels-through-a-real-production-bug</guid>
<pubDate>Thu, 16 Apr 2026 15:48:54 +0000</pubDate>
</item>
<item>
<title>Shindig — Cross-Platform Event Discovery</title>
<link>https://coderlegion.com/13717/shindig-cross-platform-event-discovery</link>
<description>&lt;p&gt;Shindig is a cross-platform event discovery app built with Kotlin Multiplatform (KMP) and Compose Multiplatform. One codebase runs on both Android and iOS with 80% shared code.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Discover events&lt;/strong&gt; happening around you from multiple aggregated sources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create and manage&lt;/strong&gt; your own events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book and join&lt;/strong&gt; events in seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subscribe to Shindig Spark&lt;/strong&gt; for premium features via in-app purchases&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kotlin Multiplatform&lt;/strong&gt; — shared business logic across Android and iOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compose Multiplatform&lt;/strong&gt; — shared UI layer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Koin&lt;/strong&gt; — dependency injection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ktor&lt;/strong&gt; — networking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DataStore&lt;/strong&gt; — local storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostHog&lt;/strong&gt; — analytics and feature flags&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GCP Secret Manager&lt;/strong&gt; — secrets management (no hardcoded keys)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; — CI/CD pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;recent-milestones&quot;&gt;Recent Milestones&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;iPadOS 26 support with auth sheet crash fixes&lt;/li&gt;
&lt;li&gt;In-app purchases live (Shindig Spark subscription)&lt;/li&gt;
&lt;li&gt;Cross-product analytics dashboard with PostHog&lt;/li&gt;
&lt;li&gt;City availability survey for out-of-market expansion&lt;/li&gt;
&lt;li&gt;Apple App Store review compliance (sign-in UX improvements)&lt;/li&gt;
&lt;li&gt;Full security audit — hardcoded secrets removed, encrypted token storage&lt;/li&gt;
&lt;li&gt;iOS bundle separation audit complete&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-kmp-&quot;&gt;Why KMP?&lt;/h3&gt;
&lt;p&gt;We chose Kotlin Multiplatform because event discovery has complex business logic (geo-spatial search, event aggregation, user preferences) but relatively standard UI patterns. KMP lets us share 80% of the code while keeping native performance and platform-specific UX where it matters.&lt;/p&gt;
&lt;p&gt;Sub-100ms geo-spatial search. One codebase. Two platforms. No compromises.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Expanding to new cities (survey-driven market entry)&lt;/li&gt;
&lt;li&gt;AI-powered event recommendations&lt;/li&gt;
&lt;li&gt;Firefly virtual economy — earn and spend currency across the platform&lt;/li&gt;
&lt;li&gt;Enhanced social features&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Built by &lt;a rel=&quot;nofollow&quot; href=&quot;https://mdostal.com&quot;&gt;Mathew Dostal&lt;/a&gt; — Fractional CTO &amp;amp; Principal Architect based in Austin, TX.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/13717/shindig-cross-platform-event-discovery</guid>
<pubDate>Mon, 30 Mar 2026 22:40:45 +0000</pubDate>
</item>
<item>
<title>CodeContext</title>
<link>https://coderlegion.com/8879/codecontext</link>
<description>&lt;p&gt;CodeContext is an innovative open-source project designed to facilitate faster developer onboarding by intelligently analyzing codebases. &lt;/p&gt;
&lt;h3 id=&quot;why-codecontext-&quot;&gt;Why CodeContext?&lt;/h3&gt;
&lt;p&gt;Onboarding new developers can often take between 1 to 3 months, during which they struggle with various challenges such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Searching for initial tasks or reference points&lt;/li&gt;
&lt;li&gt;Understanding file dependencies and relationships&lt;/li&gt;
&lt;li&gt;Navigating outdated documentation&lt;/li&gt;
&lt;li&gt;Handling repetitive questions from team members  &lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-solution&quot;&gt;The Solution&lt;/h4&gt;
&lt;p&gt;CodeContext offers a solution by quickly analyzing a codebase and providing essential insights to enhance the onboarding experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interactive Dependency Maps&lt;/strong&gt;: Visualize the entire structure of the codebase with zoomable, force-directed graphs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Knowledge Hotspots&lt;/strong&gt;: Leverage a PageRank algorithm to identify critically important files within the project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personalized Learning Paths&lt;/strong&gt;: Generate suggested reading orders to guide new developers effectively through the codebase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart Contextual Information&lt;/strong&gt;: Gain insights into Git history, authorship, and frequency of changes for each file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features collectively reduce the onboarding time significantly, converting the period from 3 months to just 3 weeks.&lt;/p&gt;
&lt;h3 id=&quot;features&quot;&gt;Features&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature                        &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Interactive Dependency Maps&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Visual representation of file relationships through dynamic graphs.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Knowledge Hotspots&lt;/strong&gt;         &lt;/td&gt;&lt;td&gt;Discover critical files based on their importance in the project using PageRank.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Learning Paths&lt;/strong&gt;             &lt;/td&gt;&lt;td&gt;Provides a topologically sorted order to help new team members navigate the project with ease.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Git Integration&lt;/strong&gt;            &lt;/td&gt;&lt;td&gt;Access detailed Git history and statistics for better context.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Team Contribution Map&lt;/strong&gt;      &lt;/td&gt;&lt;td&gt;Identify knowledge silos and assess risks related to team member dependencies.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Multi-Language Support&lt;/strong&gt;     &lt;/td&gt;&lt;td&gt;Currently supports Java and Kotlin, with plans for more languages.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fast Performance&lt;/strong&gt;           &lt;/td&gt;&lt;td&gt;Built with parallel parsing and intelligent caching for efficiency.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Visual Reports&lt;/strong&gt;             &lt;/td&gt;&lt;td&gt;Generates clean HTML reports enhanced with D3.js visualizations.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;demo&quot;&gt;Demo&lt;/h3&gt;
&lt;p&gt;An interactive demo illustrates the functionality of CodeContext by showcasing a dependency graph, knowledge hotspots, and learning path generation. To see CodeContext in action, run the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew run --args=&quot;analyze .&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;documentation&quot;&gt;Documentation&lt;/h3&gt;
&lt;p&gt;Comprehensive documentation is available to help users understand the architecture, development guidelines, API references, and more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sonii-shivansh/CodeContext/blob/main/docs/ARCHITECTURE.md&quot;&gt;Architecture Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sonii-shivansh/CodeContext/blob/main/docs/DEVELOPMENT.md&quot;&gt;Development Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sonii-shivansh/CodeContext/blob/main/docs/API.md&quot;&gt;API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;contributing&quot;&gt;Contributing&lt;/h3&gt;
&lt;p&gt;Contributions are encouraged and can take many forms, including bug fixes, feature additions, or documentation improvements. A structured guide is provided to help newcomers get started. To explore open issues suitable for new contributors, browse the issues labeled &lt;strong&gt;good first issue&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;roadmap&quot;&gt;Roadmap&lt;/h3&gt;
&lt;p&gt;Future enhancements include support for additional programming languages, integration of popular IDE plugins, and potential cloud-hosted services to extend functionality.&lt;/p&gt;
&lt;p&gt;For those interested in optimizing their development processes and enhancing team efficiency, CodeContext provides essential tools and insights to streamline the onboarding experience. Visit the project's &lt;a rel=&quot;nofollow&quot; href=&quot;https://codecontext.dev&quot;&gt;website&lt;/a&gt; for further information and resources.&lt;/p&gt;
</description>
<category>Open Source</category>
<guid isPermaLink="true">https://coderlegion.com/8879/codecontext</guid>
<pubDate>Thu, 25 Dec 2025 08:00:37 +0000</pubDate>
</item>
<item>
<title>Type-safe Kotlin Multiplatform i18n: auto-convert Android strings to cross-platform translations.</title>
<link>https://coderlegion.com/6454/type-safe-kotlin-multiplatform-i18n-convert-android-strings-cross-platform-translations</link>
<description>&lt;p&gt;i18nKonfig — Zero-Config Internationalization for Kotlin Multiplatform&lt;/p&gt;
&lt;p&gt;Tired of juggling strings.xml, runtime crashes, and messy translation setups?&lt;br&gt;
I built i18nKonfig, a Gradle plugin that turns your Android strings into type-safe, multiplatform translations — automatically &lt;/p&gt;
&lt;p&gt; From this:&lt;br&gt;
getString(R.string.welcome_message, userName)&lt;/p&gt;
&lt;p&gt; To this:&lt;br&gt;
Text(I18nKonfig.welcome_message)&lt;/p&gt;
&lt;p&gt;Works across Android, iOS, Desktop &amp;amp; Web&lt;br&gt;
 Compile-time safety (no missing strings)&lt;br&gt;
 Auto-migrates your Android resources&lt;br&gt;
 Zero manual setup — just ./gradlew build&lt;/p&gt;
&lt;p&gt;I haven’t released it yet — just testing community interest.&lt;br&gt;
Would you use something like this in your KMP projects? Or even help shape the first release?&lt;/p&gt;
</description>
<category>Open Source Tools</category>
<guid isPermaLink="true">https://coderlegion.com/6454/type-safe-kotlin-multiplatform-i18n-convert-android-strings-cross-platform-translations</guid>
<pubDate>Tue, 14 Oct 2025 23:22:55 +0000</pubDate>
</item>
<item>
<title>ImagePickerKMP 1.0.23: Controling Camera Launch on iOS with directCameraLaunch</title>
<link>https://coderlegion.com/5137/imagepickerkmp-1-0-23-controling-camera-launch-on-ios-with-directcameralaunch</link>
<description>&lt;h2 id=&quot;imagepickerkmp-1-0-23-controling-camera-launch-on-ios-with-directcameralaunch&quot;&gt;ImagePickerKMP 1.0.23: Controling Camera Launch on iOS with &lt;code&gt;directCameraLaunch&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The latest release of &lt;strong&gt;ImagePickerKMP (v1.0.23)&lt;/strong&gt; brings a small but&lt;br&gt;
powerful addition for iOS developers: a new flag called&lt;br&gt;
&lt;code&gt;directCameraLaunch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This feature gives you control over whether the image picker should&lt;br&gt;
&lt;strong&gt;open directly into the camera&lt;/strong&gt; or show the &lt;strong&gt;source selection&lt;br&gt;
dialog&lt;/strong&gt; first.&lt;/p&gt;
&lt;h3 id=&quot;the-new-flag-directcameralaunch&quot;&gt;The New Flag: &lt;code&gt;directCameraLaunch&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, the picker shows a dialog on iOS asking the user to choose&lt;br&gt;
between camera and gallery.\&lt;br&gt;
With &lt;code&gt;directCameraLaunch&lt;/code&gt;, you can now change that behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;directCameraLaunch = true&lt;/code&gt; → Skips the dialog and opens the&lt;br&gt;
&lt;strong&gt;camera immediately&lt;/strong&gt;.\&lt;/li&gt;
&lt;li&gt;&lt;code&gt;directCameraLaunch = false&lt;/code&gt; → Keeps the &lt;strong&gt;dialog&lt;/strong&gt; so the user can&lt;br&gt;
choose.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;usage-example&quot;&gt;Usage Example&lt;/h3&gt;
&lt;p&gt;Here's how you can use it with &lt;code&gt;ImagePickerLauncher&lt;/code&gt; in your Compose&lt;br&gt;
Multiplatform project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;    ImagePickerLauncher(
        config = ImagePickerConfig(
            onPhotoCaptured = { result -&amp;gt;
                capturedPhoto = result
                showCamera = false
            },
            onError = {
                showCamera = false
            },
            onDismiss = {
                showImagePicker = false // Reset state when user doesn't select anything
            },
            directCameraLaunch = false // Set to true if you want to launch the camera directly Only IOS
        )
    )

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;This new option provides &lt;strong&gt;better UX flexibility&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apps that want a streamlined flow (e.g., scanning receipts, IDs, QR&lt;br&gt;
codes) can go straight to the camera.\&lt;/li&gt;
&lt;li&gt;Apps that want to offer both gallery and camera selection can keep&lt;br&gt;
the dialog.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short: you now have the best of both worlds, controlled with a simple&lt;br&gt;
flag.&lt;/p&gt;
&lt;h3 id=&quot;dependency&quot;&gt;Dependency&lt;/h3&gt;
&lt;p&gt;To try it out, update your Gradle dependency to the latest version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies {
    implementation(&quot;io.github.ismoy:imagepickerkmp:1.0.23&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;directCameraLaunch&lt;/code&gt; flag makes ImagePickerKMP even more versatile&lt;br&gt;
for real-world use cases. Whether your app prioritizes fast camera&lt;br&gt;
access or user choice, you can now support both seamlessly with version&lt;br&gt;
&lt;strong&gt;1.0.23&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Are you planning to use &lt;code&gt;directCameraLaunch&lt;/code&gt; in your project? Would you&lt;br&gt;
prefer the dialog or direct camera launch by default?&lt;/p&gt;
&lt;p&gt;Github: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/ImagePickerKMP&quot;&gt;https://github.com/ismoy/ImagePickerKMP&lt;/a&gt;&lt;/p&gt;
</description>
<category>Building Open Source Projects</category>
<guid isPermaLink="true">https://coderlegion.com/5137/imagepickerkmp-1-0-23-controling-camera-launch-on-ios-with-directcameralaunch</guid>
<pubDate>Wed, 03 Sep 2025 05:56:05 +0000</pubDate>
</item>
<item>
<title>Simplifying Kotlin Multiplatform Setup with the New Android-KMP Plugin</title>
<link>https://coderlegion.com/5136/simplifying-kotlin-multiplatform-setup-with-the-new-android-kmp-plugin</link>
<description>&lt;h2 id=&quot;simplifying-kotlin-multiplatform-setup-with-the-new-android-kmp-plugin&quot;&gt;Simplifying Kotlin Multiplatform Setup with the New Android-KMP Plugin&lt;/h2&gt;
&lt;p&gt;Kotlin Multiplatform (KMP) has been evolving rapidly, and one of the&lt;br&gt;
latest updates brings a huge quality-of-life improvement for Android&lt;br&gt;
developers: a new Gradle plugin tailored specifically for KMP projects.&lt;/p&gt;
&lt;h3 id=&quot;what-changed-&quot;&gt;What changed?&lt;/h3&gt;
&lt;p&gt;Traditionally, when building Android libraries in a KMP setup, we used&lt;br&gt;
the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;plugins {
    id(&quot;com.android.library&quot;)
    kotlin(&quot;multiplatform&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked, but it wasn't optimized for multiplatform workflows.&lt;br&gt;
Developers often faced issues like slow builds, confusing &lt;code&gt;android {}&lt;/code&gt;&lt;br&gt;
blocks, or having to manually adjust configurations.&lt;/p&gt;
&lt;p&gt;Now, Google and JetBrains recommend switching to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;plugins {
    id(&quot;com.android.kotlin.multiplatform.library&quot;)
    kotlin(&quot;multiplatform&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-it-matters&quot;&gt;Why it matters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Faster builds -- the new plugin is optimized for KMP projects.\&lt;/li&gt;
&lt;li&gt;Cleaner configuration -- no more awkward &lt;code&gt;android {}&lt;/code&gt; blocks.\&lt;/li&gt;
&lt;li&gt;Better IDE integration -- Android Studio and IntelliJ IDEA&lt;br&gt;
understand the new plugin out of the box.\&lt;/li&gt;
&lt;li&gt;Smarter defaults -- Android instrumented tests are disabled by&lt;br&gt;
default (no more empty test setups you don't need).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short: your Gradle setup becomes leaner, and your workflow smoother.&lt;/p&gt;
&lt;h3 id=&quot;migration-example&quot;&gt;Migration example&lt;/h3&gt;
&lt;p&gt;If you already have a KMP module configured with &lt;code&gt;com.android.library&lt;/code&gt;,&lt;br&gt;
just replace it with the new plugin:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;plugins {
    id(&quot;com.android.library&quot;)
    kotlin(&quot;multiplatform&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;plugins {
    id(&quot;com.android.kotlin.multiplatform.library&quot;)
    kotlin(&quot;multiplatform&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it! No extra boilerplate, no breaking changes.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-2&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;This update may look small, but it's a big step toward making Kotlin&lt;br&gt;
Multiplatform easier and more enjoyable for Android developers. If&lt;br&gt;
you're starting a new KMP project, you should adopt the new plugin right&lt;br&gt;
away. For existing projects, the migration is painless and gives you a&lt;br&gt;
smoother development experience.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;What do you think about this change? Have you already tried the new&lt;br&gt;
plugin in your projects?&lt;/p&gt;
</description>
<category>kotlin</category>
<guid isPermaLink="true">https://coderlegion.com/5136/simplifying-kotlin-multiplatform-setup-with-the-new-android-kmp-plugin</guid>
<pubDate>Wed, 03 Sep 2025 05:55:57 +0000</pubDate>
</item>
<item>
<title>BelZSpeedScann Ismoy</title>
<link>https://coderlegion.com/5098/belzspeedscann-ismoy</link>
<description>&lt;h3 id=&quot;scan-barcodes-in-just-3-lines-with-belzspeedscan-kotlin-multiplatform-android-&quot;&gt;Scan Barcodes in Just 3 Lines with BelZSpeedScan (Kotlin Multiplatform/Android)&lt;/h3&gt;
&lt;p&gt;Scanning barcodes is a common feature in mobile apps — from retail and logistics to fintech and personal productivity. But implementing a barcode scanner that works across Android and iOS can be painful.&lt;/p&gt;
&lt;p&gt;That’s why I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/BelZSpeedScan&quot;&gt;BelZSpeedScan&lt;/a&gt;&lt;br&gt;
: a Kotlin Multiplatform library that lets you integrate a barcode scanner with just 3 lines of code.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;Add the dependency from Maven Central:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation(&quot;io.github.ismoy:belzspeedscan:1.0.12&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;usage-only-3-lines&quot;&gt;Usage: Only 3 Lines&lt;/h3&gt;
&lt;p&gt;Here’s how you can open the scanner and capture the result in your Compose Multiplatform project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; var showScanner by remember { mutableStateOf(false) } 

if (showScanner) {
    BelZSpeedScanner(
        onCodeScanned = { code -&amp;gt; 
            // Handle scanned result here
        }
    )
}

Button(onClick = { showScanner = true }) {
    Text(&quot;Scan&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it! When the user taps Scan, the camera opens, detects the barcode, and returns the result in onCodeScanned.&lt;/p&gt;
&lt;h3 id=&quot;why-belzspeedscan-&quot;&gt;Why BelZSpeedScan?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multiplatform ready: Works on Android &amp;amp; iOS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zero boilerplate: No need to handle camera APIs or decoding manually.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Production-ready: Lightweight, fast, and easy to drop into your &lt;br&gt;
project.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;get-started&quot;&gt;Get Started&lt;/h3&gt;
&lt;p&gt;Check out the full documentation and source code here:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/BelZSpeedScan&quot;&gt;https://github.com/ismoy/BelZSpeedScan&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you try it out, I’d love your feedback and contributions! ⭐&lt;/p&gt;
</description>
<category>Kotlin for Android</category>
<guid isPermaLink="true">https://coderlegion.com/5098/belzspeedscann-ismoy</guid>
<pubDate>Sat, 30 Aug 2025 04:32:45 +0000</pubDate>
</item>
<item>
<title>Android implementation</title>
<link>https://coderlegion.com/5003/android-implementation</link>
<description>&lt;h2 id=&quot;tutorial-how-to-use-imagepickerkmp-in-android-with-jetpack-compose&quot;&gt;Tutorial: How to Use ImagePickerKMP in Android with Jetpack Compose&lt;/h2&gt;
&lt;p&gt;Picking images from the gallery or capturing from the camera has always been tricky in Android: dealing with permissions, intents, and different APIs.&lt;br&gt;
With &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/ImagePickerKMP&quot;&gt;ImagePickerKMP&lt;/a&gt;&lt;/strong&gt;, you can handle it in a &lt;strong&gt;declarative and clean way&lt;/strong&gt; using &lt;strong&gt;Jetpack Compose&lt;/strong&gt;, without worrying about the platform-specific details.&lt;/p&gt;
&lt;p&gt;In this tutorial, you’ll learn how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integrate &lt;strong&gt;ImagePickerKMP&lt;/strong&gt; in an Android app with Jetpack Compose.&lt;/li&gt;
&lt;li&gt;Show a &lt;strong&gt;live camera preview&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select images from the &lt;strong&gt;gallery&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Customize the UI with the &lt;strong&gt;Customization API&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-setup-the-dependency&quot;&gt;Step 1: Setup the dependency&lt;/h3&gt;
&lt;p&gt;Add the dependency in your &lt;code&gt;build.gradle.kts&lt;/code&gt; (module level):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies {
    implementation(&quot;io.github.ismoy:imagepickerkmp:1.0.22&quot;) // Replace with the latest version
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-2-basic-usage-in-compose&quot;&gt;Step 2: Basic usage in Compose&lt;/h2&gt;
&lt;p&gt;ImagePickerKMP exposes an ImagePicker composable you can use directly.&lt;br&gt;
Here’s the minimal example to open the camera or gallery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;ImagePickerLauncher(
        config = ImagePickerConfig(
            onPhotoCaptured = { result -&amp;gt;
               // result here            },
            onError = {
                //ppermission denied error on something else
            },
            onDismiss = {
                showImagePicker = false // Reset state when user doesn't select anything
            }
        )
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ This automatically handles:&lt;/p&gt;
&lt;p&gt;Camera  permissions.&lt;/p&gt;
&lt;p&gt;Returning the result as URI / uri.&lt;/p&gt;
&lt;h2 id=&quot;step-3-ui-customization&quot;&gt;Step 3: UI Customization&lt;/h2&gt;
&lt;p&gt;ImagePickerKMP provides a Customization API so you can style your picker UI.&lt;br&gt;
For example, let’s customize the TopBar and bottom controls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;ImagePickerLauncher(
                            config = ImagePickerConfig(
                                onPhotoCaptured = { result -&amp;gt;
                                    photoResult = result
                                    showCameraPicker = false
                                    isPickerSheetVisible = false
                                },
                                onError = {
                                    showCameraPicker = false
                                    isPickerSheetVisible = false
                                },
                                onDismiss = {
                                    showCameraPicker = false
                                    isPickerSheetVisible = false
                                },
                                 cameraCaptureConfig = CameraCaptureConfig(
                                    permissionAndConfirmationConfig = PermissionAndConfirmationConfig(
                                        customConfirmationView = { photoResult, onConfirm, onRetry -&amp;gt;
                                            CustomAndroidConfirmationView(
                                                result = photoResult,
                                                onConfirm = onConfirm,
                                                onRetry = onRetry
                                            )
                                        },
                                        customDeniedDialog = { onRetry -&amp;gt;
                                            CustomPermissionDialog(
                                                title = &quot;Permission Required&quot;,
                                                message = &quot;We need access to the camera to take photos&quot;,
                                                onRetry = onRetry
                                            )
                                        },
                                        customSettingsDialog = { onOpenSettings -&amp;gt;
                                            CustomPermissionSettingsDialog(
                                                title = &quot;Go to Settings&quot;,
                                                message = &quot;Camera permission is required to capture photos. Please grant it in  settings&quot;,
                                                onOpenSettings = onOpenSettings
                                            )
                                        }
                                    )
                                )
                            )
                        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, you completely control the UI while still using the library’s camera engine.&lt;/p&gt;
&lt;h2 id=&quot;step-4-multiple-image-selection&quot;&gt;Step 4: Multiple image selection&lt;/h2&gt;
&lt;p&gt;You can also allow multiple images to be selected from the gallery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt; GalleryPickerLauncher(
        onPhotosSelected = { photos -&amp;gt;
           //result
        },
        onError = { error -&amp;gt;
           //error
        },
        onDismiss = {
           //when the result is null
        },
        allowMultiple = true, // False for single selection
        mimeTypes = listOf(&quot;image/jpeg&quot;, &quot;image/png&quot;), // Optional: filter by type
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With ImagePickerKMP you can:&lt;/p&gt;
&lt;p&gt;Easily integrate camera and gallery in Jetpack Compose.&lt;/p&gt;
&lt;p&gt;Customize the UI and user experience.&lt;/p&gt;
&lt;p&gt;Use the same API across Android &amp;amp; iOS with Kotlin Multiplatform.&lt;/p&gt;
&lt;p&gt;This saves you from handling ActivityResultContracts, Uri parsing, and permission requests manually — all while keeping your code clean and declarative.&lt;/p&gt;
</description>
<category>Setup Guide</category>
<guid isPermaLink="true">https://coderlegion.com/5003/android-implementation</guid>
<pubDate>Mon, 25 Aug 2025 22:36:56 +0000</pubDate>
</item>
<item>
<title>BelZSpeedScan: A Kotlin Multiplatform Library for Fast Document Scanning</title>
<link>https://coderlegion.com/4910/belzspeedscan-a-kotlin-multiplatform-library-for-fast-document-scanning</link>
<description>&lt;h3 id=&quot;belzspeedscan-a-kotlin-multiplatform-library-for-fast-document-scanning&quot;&gt;BelZSpeedScan: A Kotlin Multiplatform Library for Fast Document Scanning&lt;/h3&gt;
&lt;p&gt;Building mobile apps that need document scanning is often a challenge:&lt;/p&gt;
&lt;p&gt;You want high performance.&lt;/p&gt;
&lt;p&gt;You need cross-platform support (Android &amp;amp; iOS).&lt;/p&gt;
&lt;p&gt;And you don’t want to reinvent the wheel every time.&lt;/p&gt;
&lt;p&gt;That’s why I created [BelZSpeedScan][1]&lt;br&gt;
, an open-source Kotlin Multiplatform (KMP) library that brings fast, clean and reliable document scanning to your apps with minimal setup.&lt;br&gt;
 [1]: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/BelZSpeedScan&quot;&gt;https://github.com/ismoy/BelZSpeedScan&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-belzspeedscan--1&quot;&gt;Why BelZSpeedScan?&lt;/h3&gt;
&lt;p&gt;Most scanning solutions are either locked to one platform, closed-source, or too heavy to integrate. BelZSpeedScan is designed with developers in mind:&lt;/p&gt;
&lt;p&gt;✅ Cross-platform – Works on Android and iOS with a shared API.&lt;br&gt;
✅ Fast &amp;amp; lightweight – Optimized camera preview and capture pipeline.&lt;br&gt;
✅ Clean architecture – Built with separation of concerns, easy to extend.&lt;br&gt;
✅ Open source – MIT licensed and community-driven.&lt;/p&gt;
&lt;h3 id=&quot;installation-1&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;Add the dependency in your build.gradle.kts (publish on MavenCentral):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation(&quot;io.github.ismoy:belzspeedscan:1.0.12&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don’t forget to configure iOS-specific permissions in your Info.plist file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;&amp;lt;key&amp;gt;NSCameraUsageDescription&amp;lt;/key&amp;gt;
&amp;lt;string&amp;gt;We need access to the camera to scan QR.&amp;lt;/string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;quick-usage&quot;&gt;Quick Usage&lt;/h3&gt;
&lt;p&gt;Here’s how simple it is to start a camera preview and capture a scanned code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt; var showScanner by remember { mutableStateOf(false) }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;if (showScanner) {
    BelZSpeedScanner(
     onCodeScanned = { code -&amp;gt;}//result scan   
   )
}

Button(onClick = { showScanner = true }) {
    Text(&quot;Scan&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;analytics-integration&quot;&gt;Analytics Integration&lt;/h3&gt;
&lt;p&gt;BelZSpeedScan includes an optional analytics system designed with privacy in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Privacy-focused&lt;/strong&gt;: No personal data is collected&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configurable&lt;/strong&gt;: Granular control over what data is collected&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-platform&lt;/strong&gt;: Works on both Android and iOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Callbacks&lt;/strong&gt;: Support for custom analytics implementations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;contribute&quot;&gt;Contribute&lt;/h3&gt;
&lt;p&gt;BelZSpeedScan is still in its early days. If you’re building apps with Kotlin Multiplatform and need fast scanning, I’d love your feedback, issues, and contributions!&lt;/p&gt;
&lt;p&gt; I built BelZSpeedScan because I needed a reliable KMP solution for code scanning. Now I’m sharing it with the community. If you find it useful, consider comment the post and share &lt;/p&gt;
</description>
<category>Open Source Tools</category>
<guid isPermaLink="true">https://coderlegion.com/4910/belzspeedscan-a-kotlin-multiplatform-library-for-fast-document-scanning</guid>
<pubDate>Fri, 22 Aug 2025 02:21:38 +0000</pubDate>
</item>
<item>
<title>Kotlin Multiplatform + Compose: Unified Camera &amp; Gallery Picker with Expect/Actual</title>
<link>https://coderlegion.com/4886/kotlin-multiplatform-compose-unified-camera-gallery-picker-with-expect-actual</link>
<description>&lt;h3 id=&quot;kotlin-multiplatform-compose-unified-camera-amp-gallery-picker-with-expect-actual-and-permission-handling&quot;&gt;Kotlin Multiplatform + Compose: Unified Camera &amp;amp; Gallery Picker with Expect/Actual and Permission Handling&lt;/h3&gt;
&lt;p&gt;Handling image capture and gallery access in mobile apps is a task most developers face – but doing it the Kotlin Multiplatform way, using Compose, introduces unique challenges.&lt;/p&gt;
&lt;p&gt;In this post, I’ll walk through how to create a shared, composable-based image picker using Kotlin’s expect/actual mechanism. The result is a unified camera and gallery experience for Android and iOS, fully integrated with Compose Multiplatform UI.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-platform-specific-image-pickers&quot;&gt;The Problem with Platform-Specific Image Pickers&lt;/h3&gt;
&lt;p&gt;Image picking requires different APIs on Android and iOS:&lt;/p&gt;
&lt;p&gt;• Android: ActivityResultContracts, CameraX, media permissions&lt;/p&gt;
&lt;p&gt;• iOS: UIImagePickerController, delegate protocols, Info.plist&lt;/p&gt;
&lt;p&gt;• Compose Multiplatform has no built-in media picker&lt;/p&gt;
&lt;p&gt;• Permissions and file access require different logic per platform&lt;/p&gt;
&lt;p&gt;We want to hide this complexity and give developers a clean, shared interface.&lt;/p&gt;
&lt;h3 id=&quot;step-1-define-expect-functions-in-commonmain&quot;&gt;Step 1: Define expect functions in commonMain&lt;/h3&gt;
&lt;p&gt;We declare our public API with expect composables. These serve as the shared contract across platforms.&lt;/p&gt;
&lt;h3 id=&quot;imagepickerlauncher&quot;&gt;ImagePickerLauncher&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Composable
expect fun ImagePickerLauncher(
    config: ImagePickerConfig
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This picker handles capturing a photo with the camera. The config provides callbacks like onPhotoCaptured, onError, and onDismiss.&lt;/p&gt;
&lt;h3 id=&quot;gallerypickerlauncher&quot;&gt;GalleryPickerLauncher&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Composable
expect fun GalleryPickerLauncher(
    onPhotosSelected: (List&amp;lt;PhotoResult&amp;gt;) -&amp;gt; Unit,
    onError: (Exception) -&amp;gt; Unit,
    onDismiss: () -&amp;gt; Unit = {},
    allowMultiple: Boolean = false,
    mimeTypes: List&amp;lt;String&amp;gt; = listOf(&quot;image/*&quot;),
    selectionLimit: Long = SELECTION_LIMIT
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This version supports selecting one or multiple images and filtering by MIME type.&lt;/p&gt;
&lt;h3 id=&quot;android-implementation&quot;&gt;Android Implementation&lt;/h3&gt;
&lt;p&gt;The Android actual implementation uses Compose with platform-aware context handling:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Composable
actual fun ImagePickerLauncher(config: ImagePickerConfig) {
    val context = LocalContext.current
    if (context !is ComponentActivity) {
        config.onError(Exception(&quot;Invalid context&quot;))
        return
    }

    CameraCaptureView(
        activity = context,
        onPhotoResult = { result -&amp;gt; config.onPhotoCaptured(result) },
        onPhotosSelected = config.onPhotosSelected,
        onError = config.onError,
        onDismiss = config.onDismiss,
        cameraCaptureConfig = config.cameraCaptureConfig
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;for-gallery-access-&quot;&gt;For gallery access:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Composable
actual fun GalleryPickerLauncher(...) {
    val context = LocalContext.current
    if (context !is ComponentActivity) {
        onError(Exception(&quot;Invalid context&quot;))
        return
    }
    val config = GalleryPickerConfig(
        context = context,
        onPhotosSelected = onPhotosSelected,
        onError = onError,
        onDismiss = onDismiss,
        allowMultiple = allowMultiple,
        mimeTypes = mimeTypes
    )
    GalleryPickerLauncherContent(config)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ios-implementation&quot;&gt;iOS Implementation&lt;/h3&gt;
&lt;p&gt;On iOS, we integrate UIKit behavior with Compose state, launching the correct picker depending on user action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Composable
actual fun ImagePickerLauncher(config: ImagePickerConfig) {
    var showDialog by remember { mutableStateOf(true) }
    var askCameraPermission by remember { mutableStateOf(false) }
    var launchCamera by remember { mutableStateOf(false) }
    var launchGallery by remember { mutableStateOf(false) }

    handleImagePickerState(
        showDialog = showDialog,
        askCameraPermission = askCameraPermission,
        launchCamera = launchCamera,
        launchGallery = launchGallery,
        config = config,
        onDismissDialog = { showDialog = false },
        onCancelDialog = {
            showDialog = false
            config.onDismiss()
        },
        onRequestCameraPermission = { askCameraPermission = true },
        onRequestGallery = { launchGallery = true },
        onCameraPermissionGranted = {
            askCameraPermission = false
            launchCamera = true
        },
        onCameraPermissionDenied = {
            askCameraPermission = false
            config.onDismiss()
        },
        onCameraFinished = { launchCamera = false },
        onGalleryFinished = { launchGallery = false }
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;and-for-gallery-selection-&quot;&gt;And for gallery selection:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Composable
actual fun GalleryPickerLauncher(...) {
    LaunchedEffect(Unit) {
        if (allowMultiple) {
            val selectedImages = mutableListOf&amp;lt;PhotoResult&amp;gt;()
            GalleryPickerOrchestrator.launchGallery(
                onPhotoSelected = { result -&amp;gt;
                    selectedImages.add(result)
                    onPhotosSelected(selectedImages.toList())
                },
                onError = onError,
                onDismiss = onDismiss,
                allowMultiple = true,
                selectionLimit = selectionLimit
            )
        } else {
            GalleryPickerOrchestrator.launchGallery(
                onPhotoSelected = { result -&amp;gt; onPhotosSelected(listOf(result)) },
                onError = onError,
                onDismiss = onDismiss,
                allowMultiple = false,
                selectionLimit = 1
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-it-all-comes-together-in-compose&quot;&gt;How It All Comes Together in Compose&lt;/h3&gt;
&lt;p&gt;With both platform implementations hidden, your shared Compose code stays clean:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (showCamera) {
    ImagePickerLauncher(
        config = ImagePickerConfig(
            onPhotoCaptured = { photo -&amp;gt; capturedPhoto = photo },
            onError = { showError = true },
            onDismiss = { showCamera = false }
        )
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;and-for-gallery-&quot;&gt;And for gallery:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;if (showGallery) {
    GalleryPickerLauncher(
        onPhotosSelected = { photos -&amp;gt; selectedImages = photos },
        onError = { showError = true },
        onDismiss = { showGallery = false },
        allowMultiple = true
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-this-pattern-works&quot;&gt;Why This Pattern Works&lt;/h3&gt;
&lt;p&gt;• Shared UI remains fully declarative and platform-agnostic&lt;/p&gt;
&lt;p&gt;• Permissions and platform quirks are abstracted&lt;/p&gt;
&lt;p&gt;• It respects Compose principles and KMP architecture&lt;/p&gt;
&lt;p&gt;• You avoid boilerplate and platform channels&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Kotlin Multiplatform offers powerful abstractions when used correctly. By leveraging expect/actual and fully composable APIs, we've built a clean, testable, and scalable image picker that works across platforms.&lt;/p&gt;
&lt;p&gt;You can explore the full implementation in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ismoy/ImagePickerKMP&quot;&gt;ImagePickerKMP&lt;/a&gt;, an open-source project that follows these principles. Whether you use it directly or as inspiration, it's a practical example of Compose and KMP working in harmony.&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4886/kotlin-multiplatform-compose-unified-camera-gallery-picker-with-expect-actual</guid>
<pubDate>Thu, 21 Aug 2025 00:33:16 +0000</pubDate>
</item>
<item>
<title>OpenTelemetry Tracing on the JVM</title>
<link>https://coderlegion.com/4498/opentelemetry-tracing-on-the-jvm</link>
<description>&lt;p&gt;You may know I'm a big fan of OpenTelemetry. I recently finished developing a &lt;a rel=&quot;nofollow&quot; href=&quot;https://yowcon.com/melbourne-2025/masterclasses/560/gain-practical-in-depth-experience-with-observability-using-opentelemetry&quot;&gt;master class for the YOW! conference&lt;/a&gt; at the end of the year. During development, I noticed massive differences in configuration and results across programming languages. Even worse, differences exist across frameworks inside the same programming language.&lt;/p&gt;
&lt;p&gt;In this post, I want to compare the different zero-code OpenTelemetry approaches on the JVM, covering the most widespread:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot with Micrometer Tracing&lt;/li&gt;
&lt;li&gt;Spring Boot with the OpenTelemetry Agent &lt;/li&gt;
&lt;li&gt;OpenTelemetry Spring Boot Starter&lt;/li&gt;
&lt;li&gt;Quarkus&lt;/li&gt;
&lt;li&gt;Quarkus with the OpenTelemetry Agent&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;commonalities&quot;&gt;Commonalities&lt;/h3&gt;
&lt;p&gt;I keep the architecture pretty simple:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11040439081269176861&quot; alt=&quot;Architecture overview&quot;&gt;&lt;/p&gt;
&lt;p&gt;I'm using Reactive data access on both the remote service and the database to spice up things a bit, more specifically, Kotlin coroutines. Here's the general structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val products = coroutineScope {
    val ping = async {
        // Call the ping service
    }
    val products = async {
        // Query the database
    }
    println(&quot;Received ping response: ${ping.await()}&quot;)
    products.await()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the features for each stack:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Quarkus &lt;/th&gt;&lt;th&gt;Spring Boot&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Web &lt;/td&gt;&lt;td&gt;Mutiny &lt;/td&gt;&lt;td&gt;WebFlux&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;HTTP client &lt;/td&gt;&lt;td&gt;REST client &lt;/td&gt;&lt;td&gt;&lt;code&gt;WebClient&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Database access pattern &lt;/td&gt;&lt;td&gt;Record &lt;/td&gt;&lt;td&gt;Repository&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Database access &lt;/td&gt;&lt;td&gt;Hibernate Reactive with Panache &lt;/td&gt;&lt;td&gt;R2DBC&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;running-the-opentelemetry-agent&quot;&gt;Running the OpenTelemetry Agent&lt;/h3&gt;
&lt;p&gt;The OpenTelemetry Java Agent is the first approach I used regarding OpenTelemetry.&lt;/p&gt;
&lt;p&gt;The only necessary configuration is to set the agent when running the JVM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;java -javaagent:opentelemetry-javaagent.jar -jar otel-boot-agent.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent supports &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md&quot;&gt;lots of frameworks and libraries&lt;/a&gt;, including Spring Boot, Quarkus, Ktor, Spark, and many others. When the application flow finds a supported framework/library, it logs a span.&lt;/p&gt;
&lt;p&gt;The Agent upholds the standard OpenTelemetry environment variables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  otel-boot-agent:
    build: otel-boot-agent
    environment:
      OTEL_SERVICE_NAME: OTEL Boot Agent                             #1
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318                #2
      OTEL_METRICS_EXPORTER: none                                    #3
      OTEL_LOGS_EXPORTER: none                                       #3
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;OpenTelemetry service name&lt;/li&gt;
&lt;li&gt;OpenTelemetry endpoint; Spring Boot uses HTTP&lt;/li&gt;
&lt;li&gt;Neither metrics nor logs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's the Jaeger trace when calling the endpoint on the Spring Boot application:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2041424122883509039&quot; alt=&quot;Trace of Spring Boot with OpenTelemetry Agent&quot;&gt;&lt;/p&gt;
&lt;p&gt;And here's the one on Quarkus:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15781423208460660859&quot; alt=&quot;Trace of Quarkus with OpenTelemetry Agent&quot;&gt;&lt;/p&gt;
&lt;p&gt;Spring Boot features an additional span that displays the repository call. There's no such thing available on Quarkus, as I'm using the Record pattern to access the data.&lt;/p&gt;
&lt;p&gt;The Agent outputs the SQL query in both frameworks, &lt;em&gt;i.e.&lt;/em&gt;, &lt;code&gt;SELECT product.* FROM product&lt;/code&gt;. &lt;strong&gt;The Java Agent works out-of-the-box&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;micrometer-tracing-on-spring-boot&quot;&gt;Micrometer Tracing on Spring Boot&lt;/h3&gt;
&lt;p&gt;Spring Boot provides dedicated OpenTelemetry support via &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.micrometer.io/tracing/reference/&quot;&gt;Micrometer Tracing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;gt;Micrometer Tracing provides a simple facade for the most popular tracer libraries, letting you instrument your JVM-based application code without vendor lock-in. It is designed to add little to no overhead to your tracing collection activity while maximizing the portability of your tracing effort.&lt;/p&gt;
&lt;p&gt;Besides the Micrometer Tracing dependency itself, you need additional ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Spring Boot Actuator&lt;/li&gt;
&lt;li&gt;A bridge–OpenTelemetry&lt;/li&gt;
&lt;li&gt;An exporter–OpenTelemetry as well&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Configuration doesn't follow the OpenTelemetry standard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  otel-boot-micrometer:
    environment:
      SPRING_APPLICATION_NAME: OTEL Micrometer                       #1-2
      MANAGEMENT_OTLP_TRACING_ENDPOINT: http://jaeger:4318/v1/traces #1-3
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Different values from the OpenTelemetry specification&lt;/li&gt;
&lt;li&gt;The Spring application name serves as the OpenTelemetry service name&lt;/li&gt;
&lt;li&gt;Full path to the API endpoint&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The above setup doesn't register the database call. To fix it, we need an additional dependency: &lt;a rel=&quot;nofollow&quot; href=&quot;https://r2dbc.io/r2dbc-proxy/docs/current-snapshot/docs/html/&quot;&gt;R2DBC Proxy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The new trace contains the database span:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10716697642535292646&quot; alt=&quot;Trace of Quarkus with OpenTelemetry Agent&quot;&gt;&lt;/p&gt;
&lt;p&gt;You might notice another issue: calls to the service and the database are sequential, where they should be parallel. It stems from Spring Boot not handling context propagation properly to the coroutine scope. It's an underlying work from the Spring team. Subscribe to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/spring-projects/spring-framework/issues/35185&quot;&gt;GitHub issue&lt;/a&gt; if you're interested.&lt;/p&gt;
&lt;h3 id=&quot;opentelemetry-spring-boot-starter&quot;&gt;OpenTelemetry Spring Boot Starter&lt;/h3&gt;
&lt;p&gt;The OpenTelemetry project provides a &lt;a rel=&quot;nofollow&quot; href=&quot;https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/&quot;&gt;Spring Boot starter&lt;/a&gt;. You need only a single dependency, and like other starters, Spring Boot magic takes care of configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.opentelemetry.instrumentation&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;opentelemetry-spring-boot-starter&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15280324090093701492&quot; alt=&quot;Trace of Spring Boot with the OpenTelemetry Starter&quot;&gt;&lt;/p&gt;
&lt;p&gt;The result is very similar to the previous one, including the not-parallel-but-serial issue.&lt;/p&gt;
&lt;h3 id=&quot;quarkus&quot;&gt;Quarkus&lt;/h3&gt;
&lt;p&gt;We saw the results of using the OpenTelemetry Agent in the first section. It's quite straightforward to use OpenTelemetry without the Agent; you need a single dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.quarkus&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;quarkus-opentelemetry&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quarkus prefixes regular OpenTelemetry environment variable names with &lt;code&gt;QUARKUS_&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  otel-quarkus:
    environment:
      QUARKUS_OTEL_SERVICE_NAME: OTEL Quarkus                        #1
      QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://jaeger:4317 #2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;OpenTelemetry service name&lt;/li&gt;
&lt;li&gt;OpenTelemetry endpoint; Quarkus uses gRPC&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Results are as expected:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6128337141775866164&quot; alt=&quot;Trace of Quarkus&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;discussion&quot;&gt;Discussion&lt;/h3&gt;
&lt;p&gt;OpenTelemetry approaches vary widely in both configuration and results. Unless you're prevented from using Java agents for technical or organizational reasons, I recommend using the OpenTelemetry Agent first. It handles everything you can throw at it out of the box, including the most common libraries. Barring that, you need deep knowledge of the stack you're using, lest results don't represent what happens in reality.&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/otel-jvm&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://opentelemetry.io/docs/zero-code/java/agent/&quot;&gt;OpenTelemetry Java Agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/kotlin-coroutines-otel-tracing/&quot;&gt;Kotlin Coroutines and OpenTelemetry tracing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/spring-projects/spring-framework/issues/35185&quot;&gt;Add support for Micrometer context propagation in Kotlin coroutines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/opentelemetry-tracing-jvm/&quot;&gt;A Java Geek&lt;/a&gt; on August 3&lt;sup&gt;rd&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4498/opentelemetry-tracing-on-the-jvm</guid>
<pubDate>Thu, 07 Aug 2025 17:24:20 +0000</pubDate>
</item>
<item>
<title>Pattern-matching across different languages</title>
<link>https://coderlegion.com/4260/pattern-matching-across-different-languages</link>
<description>&lt;p&gt;Pattern matching is a major feature in software development. While pattern matching applies in several locations, its current usage is limited to &lt;code&gt;switch case&lt;/code&gt; blocks. I want to compare the power of pattern matching across a couple of programming languages I'm familiar with in this post.&lt;/p&gt;
&lt;p&gt;I assume that every reader is familiar with the &lt;code&gt;switch case&lt;/code&gt; syntax inherited from C. In short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;switch&lt;/code&gt; clause references a value-returning statement&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;case&lt;/code&gt; clause sets another statement; if the value matches the statement, it executes the related block&lt;/li&gt;
&lt;li&gt;&lt;code&gt;case&lt;/code&gt; clauses are evaluated in order. The first clause that matches gets its block executed&lt;/li&gt;
&lt;li&gt;In C, &lt;code&gt;case&lt;/code&gt; clauses are fall-through; you need to explicitly &lt;code&gt;break&lt;/code&gt; to escape the &lt;code&gt;switch&lt;/code&gt;, otherwise, the next &lt;code&gt;case&lt;/code&gt; is evaluated&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;java-s-pattern-matching&quot;&gt;Java's pattern matching&lt;/h3&gt;
&lt;p&gt;I'll start with Java, as it was the first programming language I used in a professional context.&lt;/p&gt;
&lt;p&gt;Java's pattern matching has evolved a lot across its versions. Here's the &quot;official&quot; sample for version 23, slightly amended. It defines several &lt;code&gt;Shape&lt;/code&gt; implementations and a &lt;code&gt;static&lt;/code&gt; method to evaluate its perimeter. It's definitely &lt;strong&gt;not&lt;/strong&gt; a good example of Object-Oriented Programming.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }

public class Main {
    static double getPerimeter(Shape s) throws IllegalArgumentException {
        return switch (s) {                                                 //1
            case Rectangle r when r.length() == r.width() -&amp;gt; {              //2
                System.out.println(&quot;Square detected&quot;);
                    yield 4 * r.length();
            }
            case Rectangle r -&amp;gt;                                             //3
                2 * r.length() + 2 * r.width();
            case Circle c -&amp;gt;                                                //4
                2 * c.radius() * Math.PI;
            default -&amp;gt;                                                      //5
                throw new IllegalArgumentException(&quot;Unrecognized shape&quot;);
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Reference the &lt;code&gt;Shape&lt;/code&gt; parameter as &lt;code&gt;s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; whether the &lt;code&gt;Rectangle&lt;/code&gt; is a square&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Circle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If none of the previous clauses match, default to throwing an exception&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This Java version will be our baseline.&lt;/p&gt;
&lt;h3 id=&quot;characteristics-of-the-new-switch-syntax&quot;&gt;Characteristics of the new &lt;code&gt;switch&lt;/code&gt; syntax&lt;/h3&gt;
&lt;p&gt;The new syntax has some advantages over the legacy non-arrow &lt;code&gt;switch&lt;/code&gt; inherited from C.&lt;/p&gt;
&lt;p&gt;C-style &lt;code&gt;case&lt;/code&gt; clauses are fall-through. Once the runtime has executed a &lt;code&gt;case&lt;/code&gt; block, it runs the next &lt;code&gt;case&lt;/code&gt; block. To avoid it, you need to set a &lt;code&gt;break&lt;/code&gt; explicitly. In the not-so-good old days, some developers leveraged this feature to avoid code duplication. However, experience has shown that it was too much of a cause of potential bugs: modern coding practices tend to avoid it. Yet, it's too easy to forget a &lt;code&gt;break&lt;/code&gt;. With the new arrow syntax, the runtime only evaluates the corresponding &lt;code&gt;case&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;C-style &lt;code&gt;case&lt;/code&gt; clauses only evaluate simple values. The community celebrated Java 7 as a huge boon when it allowed String values. Java 21 went even further: coupled with the arrow syntax, it allowed for switching on types. In the above example, we check the exact &lt;code&gt;Shape&lt;/code&gt; type. &lt;/p&gt;
&lt;p&gt;Additionally, if switching on classes &lt;em&gt;and&lt;/em&gt; if the class hierarchy is &lt;code&gt;sealed&lt;/code&gt;, the compiler can automatically detect missing cases. Finally, version 24 added the additional optional &lt;code&gt;when&lt;/code&gt; filter.&lt;/p&gt;
&lt;p&gt;On the flip side, in the old C syntax, the runtime jumps directly to the correct &lt;code&gt;case&lt;/code&gt; clause. The new arrow syntax evaluates them sequentially, exactly like if with &lt;code&gt;if else&lt;/code&gt; statements.&lt;/p&gt;
&lt;p&gt;In the next sections, we will port the code to other languages.&lt;/p&gt;
&lt;h3 id=&quot;scala-s-pattern-matching&quot;&gt;Scala's pattern matching&lt;/h3&gt;
&lt;p&gt;Scala's pattern matching has been second to none since its inception. Kotlin drew a lot of inspiration from it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-scala&quot;&gt;trait Shape
case class Rectangle(length: Double, width: Double) extends Shape
case class Circle(radius: Double) extends Shape

def getPerimeter(s: Shape) = {
  s match {                                                                 //1
    case Rectangle(length, width) if length == width =&amp;gt;                     //2-3
      println(&quot;Square detected&quot;)
      4 * length
    case Rectangle(length, width) =&amp;gt; 2 * length + 2 * width                 //3-4
    case Circle(radius) =&amp;gt; 2 * radius * Math.PI                             //3-5
    case _ =&amp;gt; throw new IllegalArgumentException(&quot;Unrecognized shape&quot;)      //6
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Use the &lt;code&gt;s&lt;/code&gt; reference directly&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt; and whether the &lt;code&gt;Rectangle&lt;/code&gt; is a square&lt;/li&gt;
&lt;li&gt;Scala additionally matches the class attributes&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Circle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If none of the previous clauses match, default to throwing an exception&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;kotlin-s-pattern-matching&quot;&gt;Kotlin's pattern matching&lt;/h3&gt;
&lt;p&gt;Let's translate Java's code to Kotlin. For that, we &lt;em&gt;must&lt;/em&gt; activate the &lt;em&gt;experimental&lt;/em&gt; &lt;code&gt;Xwhen-guards&lt;/code&gt; compilation feature described in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Kotlin/KEEP/issues/371&quot;&gt;KEEP-371&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;interface Shape
data class Rectangle(val length: Double, val width: Double): Shape
data class Circle(val radius: Double): Shape

fun getPerimeter(s: Shape) = when (s) {                                     //1
        is Rectangle if s.length == s.width -&amp;gt; {                            //2
            println(&quot;Square detected&quot;)
            4 * s.length
        }
        is Rectangle -&amp;gt; 2 * s.length + 2 * s.width                          //3
        is Circle -&amp;gt; 2 * s.radius * Math.PI                                 //4
        else -&amp;gt; throw IllegalArgumentException(&quot;Unknown shape&quot;)             //5
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Reference the &lt;code&gt;Shape&lt;/code&gt; parameter as &lt;code&gt;s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; whether the &lt;code&gt;Rectangle&lt;/code&gt;'s &lt;code&gt;width&lt;/code&gt; is equal to its &lt;code&gt;height&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Rectangle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Evaluate whether &lt;code&gt;s&lt;/code&gt; is a &lt;code&gt;Circle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If none of the previous clauses match, default to throwing an exception&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kotlin's pattern matching greatly resembles Java's, with slight syntax changes, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;if&lt;/code&gt; replaces &lt;code&gt;when&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The comparative evolution of Kotlin vs. Java in pattern matching is quite enlightening. Java lagged behind the legacy C syntax, while Kotlin could already match on concrete types from the beginning. In 2020, Java 14 reduced the gap with the arrow operator; one year later, Java 16 closed the gap completely with the ability to match on concrete types. Finally, Java 23 added the &lt;code&gt;when&lt;/code&gt; clause.&lt;/p&gt;
&lt;p&gt;The experimental nature of Kotlin's &lt;code&gt;if&lt;/code&gt; means Java has overtaken Java–at least in this area! It's rare enough to be noted.&lt;/p&gt;
&lt;h3 id=&quot;python-s-pattern-matching&quot;&gt;Python's pattern matching&lt;/h3&gt;
&lt;p&gt;Before, Python didn't offer anything similar to the &lt;code&gt;switch&lt;/code&gt; statement of the above JVM languages. From version 3.10, it does offer the same capability in an elegant way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Shape:
    pass


class Rectangle(Shape):
    def __init__(self, length: float, width: float):
        self.length = length
        self.width = width


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius


def get_perimeter(s: Shape) -&amp;gt; float:
    match s:
        case Rectangle(length=l, width=w) if l == w:
            print(&quot;Square detected&quot;)
            return 4 * l
        case Rectangle(length=l, width=w):
            return 2 * l + 2 * w
        case Circle(radius=r):
            return 2 * pi * r
        case _:
            raise ValueError(&quot;Unknown shape&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runtime evaluates the &lt;code&gt;case&lt;/code&gt; clauses sequentially, as in the JVM languages.&lt;/p&gt;
&lt;h3 id=&quot;rust-s-pattern-matching&quot;&gt;Rust's pattern matching&lt;/h3&gt;
&lt;p&gt;Rust's approach to memory management doesn't play well with checking types. In short, Rust offers two base concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structures are structured data placeholders; their memory size is known at compile time&lt;/li&gt;
&lt;li&gt;Traits are contracts, akin to interfaces&lt;/li&gt;
&lt;li&gt;You can provide an implementation of a trait for a structure&lt;/li&gt;
&lt;li&gt;Referencing a variable by its trait has consequences.&lt;br&gt;
Since the reference can point to structures of different sizes, the compiler can't know its size at compile-time and must place the variable on the heap instead of the stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried to port the original code to Rust one-to-one for educational purposes. The original Java doesn't take advantage of polymorphism; it is not great. In Rust, it's even uglier. It's interesting to realize that while Rust isn't an Object-Oriented programming language, it leads you to use polymorphism.&lt;/p&gt;
&lt;p&gt;Warning: I don't recommend the following code and will deny any association with it!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;trait Shape: Any {
    fn as_any(&amp;amp;self) -&amp;gt; &amp;amp;dyn Any;                                     //1
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn as_any(&amp;amp;self) -&amp;gt; &amp;amp;dyn Any {                                    //2
        self
    }
}
impl Shape for Rectangle {
    fn as_any(&amp;amp;self) -&amp;gt; &amp;amp;dyn Any {                                    //2
        self
    }
}

fn get_perimeter(s: Box&amp;lt;dyn Shape&amp;gt;) -&amp;gt; f64 {
    match s.as_any() {                                                //3
        any if any.is::&amp;lt;Rectangle&amp;gt;() =&amp;gt; {                             //4
            let rectangle = any.downcast_ref::&amp;lt;Rectangle&amp;gt;().unwrap(); //5
            if rectangle.width == rectangle.height {
                println!(&quot;Square matched&quot;);
                4.0 * rectangle.width
            } else {
                2.0 * (rectangle.width + rectangle.height)
            }
        }
        any if any.is::&amp;lt;Circle&amp;gt;() =&amp;gt; {                                //4
            let circle = any.downcast_ref::&amp;lt;Circle&amp;gt;().unwrap();       //5
            2.0 * std::f64::consts::PI * circle.radius
        }
        _ =&amp;gt; panic!()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Rust's type system doesn't offer a way to get the type of a variable. We must create a dedicated function for that.&lt;/li&gt;
&lt;li&gt;Implement the function for structures&lt;/li&gt;
&lt;li&gt;Match on the underlying structure type&lt;/li&gt;
&lt;li&gt;Check the type&lt;/li&gt;
&lt;li&gt;Downcast to the underlying structure to use its fields&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This artificial code port above misrepresents Rust's pattern-matching abilities. It applies in &lt;a rel=&quot;nofollow&quot; href=&quot;https://doc.rust-lang.org/book/ch19-01-all-the-places-for-patterns.html&quot;&gt;many places&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Among all languages described in the post, Scala was the first to provide pattern-matching in &lt;code&gt;switch&lt;/code&gt; clauses. For many years, it was the Grail that others tried to catch up with; Kotlin and Java have finally reached this stage.&lt;/p&gt;
&lt;p&gt;Outside of the JVM, Python and Rust feature powerful pattern-matching capabilities.&lt;/p&gt;
&lt;p&gt;With destructuring, pattern matching is a huge help to developers who want to write readable and maintainable code.&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/pattern-matching&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.oracle.com/en/java/javase/23/language/pattern-matching.html&quot;&gt;Pattern matching in Java 23&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/control-flow.html#when-expressions-and-statements&quot;&gt;Pattern matching in Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.scala-lang.org/tour/pattern-matching.html&quot;&gt;Pattern matching in Scala&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3.10/tutorial/controlflow.html?highlight=match#match-statements&quot;&gt;Match statement in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://doc.rust-lang.org/book/ch19-01-all-the-places-for-patterns.html&quot;&gt;Pattern matching in Rust&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/pattern-matching-different-languages/&quot;&gt;A Java Geek&lt;/a&gt; on July 20&lt;sup&gt;th&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4260/pattern-matching-across-different-languages</guid>
<pubDate>Thu, 24 Jul 2025 19:51:56 +0000</pubDate>
</item>
<item>
<title>RingtoneSmartKit – Android Ringtone Tool</title>
<link>https://coderlegion.com/4081/ringtonesmartkit-android-ringtone-tool</link>
<description>&lt;h2 id=&quot;ringtonesmartkit-effortless-android-ringtone-management-with-kotlin&quot;&gt;RingtoneSmartKit — Effortless Android Ringtone Management with Kotlin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Tired of fragmented and inconsistent ringtone management in your Android apps? RingtoneSmartKit is the modern, extensible Kotlin library I built to simplify setting and managing system ringtones, alarms, notifications, and custom contact tones with ease.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We built this because Android's native ringtone handling is often&lt;br&gt;
fragmented and complex. We wanted a library that just works—no fuss,&lt;br&gt;
no boilerplate, no Context juggling.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-makes-it-special-&quot;&gt;What Makes it Special?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Effortless Control: Easily set ringtones for calls, alarms, and notifications.&lt;/li&gt;
&lt;li&gt;Contact-Specific Tones: Assign unique tones to contacts with zero hassle.&lt;/li&gt;
&lt;li&gt;Flexible Audio Sources: Handle any audio source, whether from your app's assets or content URIs.&lt;/li&gt;
&lt;li&gt;Context-Free API: Use it anywhere in your app without needing to pass a Context or Activity.&lt;/li&gt;
&lt;li&gt;Clean Architecture: Built on robust principles for future-proof extensibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;see-how-simple-it-is&quot;&gt;See How Simple It Is&lt;/h2&gt;
&lt;p&gt;Setting a system ringtone or customizing one for a specific contact is straightforward:&lt;/p&gt;
&lt;h3 id=&quot;set-a-system-ringtone&quot;&gt;Set a System Ringtone&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RingtoneHelper.setSystemRingtone(
    source = RingtoneSource.FromAssets(&quot;ringtones/my_ringtone.mp3&quot;),
    target = SystemTarget.Call // or Notification, Alarm
).onSuccess {
    println(&quot;✅ System ringtone set successfully&quot;)
}.onFailure { error -&amp;gt;
    println(&quot;❌ Error setting system ringtone: ${error.message}&quot;)
}.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;set-a-ringtone-for-a-specific-contact&quot;&gt;Set a Ringtone for a Specific Contact&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RingtoneHelper.setContactRingtone(
    source = RingtoneSource.FromStorage(Uri.parse(&quot;content://media/internal/audio/media/10&quot;)),
    target = ContactTarget.ByPhone(&quot;+1234567890&quot;)
).onSuccess { contact -&amp;gt;
    println(&quot;✅ Ringtone set for contact: ${contact.displayName}&quot;)
}.onFailure { error -&amp;gt;
    println(&quot;❌ Error setting contact ringtone: ${error.message}&quot;)
}.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;enjoy-&quot;&gt;Enjoy!&lt;/h2&gt;
&lt;p&gt;Happy coding! &lt;/p&gt;
</description>
<category>Mobile Apps</category>
<guid isPermaLink="true">https://coderlegion.com/4081/ringtonesmartkit-android-ringtone-tool</guid>
<pubDate>Thu, 17 Jul 2025 00:33:22 +0000</pubDate>
</item>
<item>
<title>A Kotlin library that simplifies setting and managing Android ringtones programmatically</title>
<link>https://coderlegion.com/4039/kotlin-library-that-simplifies-setting-and-managing-android-ringtones-programmatically</link>
<description>&lt;h2 id=&quot;ringtonesmartkit-simplified-android-ringtone-management&quot;&gt;RingtoneSmartKit – Simplified Android Ringtone Management&lt;/h2&gt;
&lt;p&gt;Hi devs! &lt;br&gt;
I recently developed and released &lt;strong&gt;RingtoneSmartKit&lt;/strong&gt;, an open-source Kotlin library that simplifies ringtone management on Android devices — with a modern, clean architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Set system ringtones&lt;/strong&gt; for alarms, notifications, and incoming calls effortlessly  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assign custom ringtones&lt;/strong&gt; to individual contacts  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Support multiple audio sources&lt;/strong&gt; like assets, raw resources, and content URIs  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context-free usage&lt;/strong&gt; — no need to pass Context or Activity explicitly  &lt;/li&gt;
&lt;li&gt;Built for &lt;strong&gt;extensibility and clean architecture&lt;/strong&gt;, ensuring long-term maintainability  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-ringtonesmartkit-&quot;&gt;Why RingtoneSmartKit?&lt;/h3&gt;
&lt;p&gt;Android doesn’t offer a simple, reusable way to manage ringtones programmatically.&lt;br&gt;
This library fills that gap by providing a &lt;strong&gt;developer-friendly API&lt;/strong&gt; that handles the heavy lifting behind the scenes.&lt;/p&gt;
&lt;p&gt;You can use it from anywhere in your codebase — without boilerplate or system-specific quirks.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next-&quot;&gt;What’s Next?&lt;/h3&gt;
&lt;p&gt;I’m exploring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vibration pattern support&lt;/strong&gt; (thanks to a great suggestion!)&lt;/li&gt;
&lt;li&gt;Enhanced customization options&lt;/li&gt;
&lt;li&gt;More examples in Compose, Java, and XML&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;contribute-amp-feedback&quot;&gt;Contribute &amp;amp; Feedback&lt;/h3&gt;
&lt;p&gt;I welcome your &lt;strong&gt;feedback, ideas, and contributions&lt;/strong&gt; to make this library even better.&lt;br&gt;
Check out the GitHub repo and feel free to open issues or pull requests.&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/amjdalhashede/RingtoneSmartKit&quot;&gt;GitHub Repository&lt;/a&gt;   &lt;/p&gt;
&lt;p&gt;Thanks for reading, and happy coding! &lt;/p&gt;
</description>
<category>Android</category>
<guid isPermaLink="true">https://coderlegion.com/4039/kotlin-library-that-simplifies-setting-and-managing-android-ringtones-programmatically</guid>
<pubDate>Tue, 15 Jul 2025 13:02:59 +0000</pubDate>
</item>
<item>
<title>Testing an OpenRewrite recipe</title>
<link>https://coderlegion.com/3508/testing-an-openrewrite-recipe</link>
<description>&lt;p&gt;For the last two weeks, I've kicked the tires of OpenRewrite. At first, I created a &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/1/&quot;&gt;recipe to move Kotlin source files&lt;/a&gt; as per the official recommendations with a set package name. I then improved the &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/2/&quot;&gt;recipe to compute the root automatically&lt;/a&gt;. In both versions, I thoroughly tested the recipe. However, my testing approach was wrong. In this post, I want to describe my mistakes, and how I fixed them.&lt;/p&gt;
&lt;h3 id=&quot;the-naive-approach&quot;&gt;The naive approach&lt;/h3&gt;
&lt;p&gt;I originally approached the testing of the recipe in a very naive way, to say the least. As explained in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/1/#testing-the-recipe&quot;&gt;first post&lt;/a&gt;, I used OpenRewrite's low-level APIs. Here's what I wrote:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Given
val parser = KotlinParser.builder().build()                                      //1
val cu = parser.parse(
    InMemoryExecutionContext(),                                                  //2
    sourceCode
).findFirst()                                                                    //3
 .orElseThrow { IllegalStateException(&quot;Failed to parse Kotlin file&quot;) }           //3
val originalPath = Paths.get(originalPath)
val modifiedCu = (cu as K.CompilationUnit).withSourcePath(originalPath)          //4

// When
val recipe = FlattenStructure(configuredRootPackage)
val result = recipe.visitor.visit(modifiedCu, InMemoryExecutionContext())        //5

// Then
val expectedPath = Paths.get(expectedPath)
assertEquals(expectedPath, (result as SourceFile).sourcePath)                    //6
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Build the Kotlin parser&lt;/li&gt;
&lt;li&gt;Set an execution context; I had to choose and the in-memory one was the easiest.&lt;/li&gt;
&lt;li&gt;Boilerplate to get the single compilation unit from the stream&lt;/li&gt;
&lt;li&gt;Cast to a &lt;code&gt;K.CompilationUnit&lt;/code&gt; because we know better&lt;/li&gt;
&lt;li&gt;Explicitly call the visitor to visit&lt;/li&gt;
&lt;li&gt;Assert the recipe moved the file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The above works, but requires a deep understanding of how OpenRewrite works. I didn't have that understanding, but it was good enough. It came back to bite me when I improved the recipe to compute the root.&lt;/p&gt;
&lt;p&gt;As explained in the last post, I switched from a regular recipe to a scanning recipe. I had to provide at least two source files to test the new capability. I came up with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// When
val recipe = FlattenStructure()
val context = InMemoryExecutionContext()
val acc = AtomicReference//String?(null)
recipe.getScanner(acc).visit(modifiedCu1, context)                               //1
recipe.getScanner(acc).visit(modifiedCu2, context)                               //1
val result1 = recipe.getVisitor(acc).visit(modifiedCu1, context)                 //2
val result2 = recipe.getVisitor(acc).visit(modifiedCu2, context)                 //2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Get the scanner and visit source files to compute the root&lt;/li&gt;
&lt;li&gt;Get the visitor and visit source files to move the file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It worked, but I admit it was a lucky guess. More involved recipes would require a deeper knowledge of how OpenRewrite works, with more potential bugs. Fortunately, OpenRewrite provides the means to keep the testing code at the right level of abstraction.&lt;/p&gt;
&lt;h3 id=&quot;the-nominal-approach&quot;&gt;The nominal approach&lt;/h3&gt;
&lt;p&gt;The nominal approach involves a couple of out-of-the-box classes; it requires a new dependency. I didn't do it before, so now is a good time: let's introduce a &lt;abbr title=&quot;Bill Of Material&quot;&gt;BOM&lt;/abbr&gt; to align all of OpenRewrite's dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.openrewrite.recipe&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;rewrite-recipe-bom&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.9.0&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's now possible to add the dependency without a version, as Maven resolves it from the above BOM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.openrewrite&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;rewrite-test&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This brings a couple of new classes to the project:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5041497665654198418&quot; alt=&quot;OpenRewrite recipes test class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;The documentation states your test class should inherit from &lt;code&gt;RewriteTest&lt;/code&gt;, which provides &lt;code&gt;rewriteRun&lt;/code&gt;. The latter runs the recipe, without any need to know about its inner workings, &lt;em&gt;e.g.&lt;/em&gt;, the above in-memory execution context. It's the abstraction level that we want. &lt;code&gt;Assertions&lt;/code&gt; offers static methods to assert. OpenRewrite also advises using Assertion4J, which I fully endorse. Yet, I didn't do it to keep the comparison simpler.&lt;/p&gt;
&lt;p&gt;We can rewrite the previous snippet to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;rewriteRun(                                                                     //1
    kotlin(sourceCode1) { spec -&amp;gt;                                               //2-3-4
        spec.path(&quot;src/main/kotlin/ch/frankel/blog/foo/Foo.kt&quot;)                 //5
        spec.afterRecipe {                                                      //6
            assertEquals(                                                       //7
                Paths.get(&quot;src/main/kotlin/foo/Foo.kt&quot;),
                it.sourcePath
            )
        }
    },
    kotlin(sourceCode2) { spec -&amp;gt;                                               //2-3-4
        spec.path(&quot;src/main/kotlin/org/frankel/blog/bar/Bar.kt&quot;)                //5
        spec.afterRecipe {                                                      //6
            assertEquals(                                                       //7
                Paths.get(&quot;src/main/kotlin/bar/Bar.kt&quot;),
                it.sourcePath
            )
        }
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Run the recipe&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kotlin&lt;/code&gt; transform the string into a &lt;code&gt;SourceSpecs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I'm using Kotlin, but &lt;code&gt;java&lt;/code&gt; does the same for regular Java projects&lt;/li&gt;
&lt;li&gt;Allow customizing the source specification&lt;/li&gt;
&lt;li&gt;Customize the path&lt;/li&gt;
&lt;li&gt;Hook after running the recipe&lt;/li&gt;
&lt;li&gt;Assert that the recipe updated the path according to the expectations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You may have noticed that the rewritten code doesn't specify which recipe it's testing. That's the responsibility of the &lt;code&gt;RewriteTest.defaults()&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FlattenStructureComputeRootPackageTest : RewriteTest {

    override fun defaults(spec: RecipeSpec) {
        spec.recipe(FlattenStructure())
    }

    // Rest of the class
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;don-t-forget-cycles&quot;&gt;Don't forget cycles&lt;/h3&gt;
&lt;p&gt;If you followed the above instructions, there's a high chance your test fails with this error message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.AssertionError: Expected recipe to complete in 0 cycle, but took 1 cycle. This usually indicates the recipe is making changes after it should have stabilized.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to turn to the documentation to understand this cryptic message:&lt;/p&gt;
&lt;p&gt;&amp;gt;The recipes in the execution pipeline may produce changes that in turn cause another recipe to do further work. As a result, the pipeline may perform multiple passes (or cycles) over all the recipes in the pipeline again until either no changes are made in a pass or some maximum number of passes is reached (by default 3). This allows recipes to respond to changes made by other recipes which execute after them in the pipeline.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#execution-cycles&quot;&gt;Execution Cycles&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because the recipe doesn't rely on any other and no other recipe depends on it, we can set the cycle to 1.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;override fun defaults(spec: RecipeSpec) {
    spec.recipe(FlattenStructure())
        .cycles(1)                                                              //1
        .expectedCyclesThatMakeChanges(1)                                       //2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Set how many cycles the recipe should run&lt;/li&gt;
&lt;li&gt;Set to 0 if the recipe isn't expected to make changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;criticisms&quot;&gt;Criticisms&lt;/h3&gt;
&lt;p&gt;I like what the OpenRewrite testing classes bring, but I have two criticisms.&lt;/p&gt;
&lt;p&gt;First and foremost, why does OpenRewrite assert the number of cycles by default? It bit me in the back for no good reason. I had to dig into the documentation and understand how OpenRewrite works, although the testing API is supposed to shield users from its inner workings. I also can't help but wonder about the defaults.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class RecipeSpec {

    @Nullable
    Integer cycles;

    int getCycles() {
        return cycles == null ? 2 : cycles;                                     //1
    }

    int getExpectedCyclesThatMakeChanges(int cycles) {
        return expectedCyclesThatMakeChanges == null ? cycles - 1 :             //2
                expectedCyclesThatMakeChanges;
    }

    // Rest of the class body
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Why two cycles by default? Shouldn't one be enough in most cases?&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;cycles - 1&lt;/code&gt; by default?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My second criticism is about how the provided testing classes make you structure your tests. I like to structure them into three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Given: describe the initial state&lt;/li&gt;
&lt;li&gt;When: execute the to-be-tested code&lt;/li&gt;
&lt;li&gt;Then: assert the final state conforms to what I expect.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With OpenRewrite's abstractions, the structure is widely different from the above.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this post, I migrated my &lt;em&gt;ad hoc&lt;/em&gt; test code to rely on OpenRewrite's provided classes. Even though they are not exempt from criticism, they offer a solid abstraction layer and make tests more maintainable.&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/flatten-kotlin-recipe&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/authoring-recipes/recipe-testing&quot;&gt;Recipe testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#execution-cycles&quot;&gt;Execution Cycles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bitflippers.dev/how-to-resolve-expected-recipe-cycle-mismatch&quot;&gt;How to resolve expected recipe cycle mismatch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/3/&quot;&gt;A Java Geek&lt;/a&gt; on June 22&lt;sup&gt;nd&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3508/testing-an-openrewrite-recipe</guid>
<pubDate>Thu, 26 Jun 2025 20:18:12 +0000</pubDate>
</item>
<item>
<title>Improving my previous OpenRewrite recipe</title>
<link>https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</link>
<description>&lt;p&gt;I started discovering OpenRewrite last week by writing a Kotlin recipe that moves Kotlin files according to the official directory structure recommendation. I mentioned some future works, and here they are. In this post, I want to describe how to compute the root package instead of letting the user set it.&lt;/p&gt;
&lt;h3 id=&quot;reminder&quot;&gt;Reminder&lt;/h3&gt;
&lt;p&gt;I developed last week a recipe to follow the Kotlin recommendation regarding directory structure:&lt;/p&gt;
&lt;p&gt;&amp;gt;In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the &lt;code&gt;org.example.kotlin&lt;/code&gt; package and its subpackages, files with the &lt;code&gt;org.example.kotlin&lt;/code&gt; package should be placed directly under the source root, and files in &lt;code&gt;org.example.kotlin.network.socket&lt;/code&gt; should be in the network/socket subdirectory of the source root.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/coding-conventions.html#source-code-organization&quot;&gt;Directory structure&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a Java project, if you have packages &lt;code&gt;ch.frankel.foo&lt;/code&gt;, &lt;code&gt;ch.frankel.bar&lt;/code&gt;, and &lt;code&gt;ch.frankel.baz&lt;/code&gt;, you'll get the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ java
       |__ ch
          |__ frankel
             |__ foo
             |__ bar
             |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Kotlin projects, many developers follow the same structure as above, but it can be flattened as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ kotlin
       |__ foo
       |__ bar
       |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-work&quot;&gt;The work&lt;/h3&gt;
&lt;p&gt;My recipe's original version mandated that you configure the root package yourself, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel&lt;/code&gt; for the above example. However, it should be possible to compute it automatically, from looking at the source files. It adds an extra step to the process: before moving the file to the root, the recipe should look at each source file, get the package, compute the longest prefix with the existing root, make it the root, and go to the next source file. The regular &lt;code&gt;Recipe&lt;/code&gt; doesn't work in this case. We need to switch to a &lt;code&gt;ScanningRecipe&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&amp;gt;If a recipe needs to generate new source files or needs to see all source files before making changes, it must be a &lt;code&gt;ScanningRecipe&lt;/code&gt;. A &lt;code&gt;ScanningRecipe&lt;/code&gt; extends the normal &lt;code&gt;Recipe&lt;/code&gt; and adds two key objects: an &lt;code&gt;accumulator&lt;/code&gt; and a &lt;code&gt;scanner&lt;/code&gt;. The &lt;code&gt;accumulator&lt;/code&gt; object is a custom data structure defined by the recipe itself to store any information the recipe needs to function. The &lt;code&gt;scanner&lt;/code&gt; object is a visitor which populates the &lt;code&gt;accumulator&lt;/code&gt; with data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning recipes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Scanning recipes offer two steps: the first to gather data, the second to do the work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14758325757968137092&quot; alt=&quot;ScanningRecipe class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;We must design our algorithm within the constraints of OpenRewrite, and they are the following: in the first phase, for each source file, OpenRewrite will call the &lt;code&gt;getScanner()&lt;/code&gt; method that returns a visitor of our choice. In turn, OpenRewrite calls the visitor's methods, which can access the accumulator.&lt;/p&gt;
&lt;p&gt;My first naive approach was to use a collection as the accumulator, but it's not necessary. The algorithm is much simpler if we set a mutable placeholder that holds the package root and update it if necessary during each visit. The initial value should be &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the value is &lt;code&gt;null&lt;/code&gt;, which happens on the first visitor, set the package root to the source file's package.&lt;/li&gt;
&lt;li&gt;If the value is an empty string, skip–see below.&lt;/li&gt;
&lt;li&gt;In any other case, compute the new package root by finding the longest prefix between the existing package root and the source file's package.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It might result in an empty string, indicating that packages have no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FlattenStructure(private val rootPackage: String?) : ScanningRecipe&amp;lt;AtomicReference&amp;lt;String?&amp;gt;&amp;gt;() { //1-2

  constructor() : this(null)                                                   //3

  override fun getDisplayName(): String = &quot;Flatten Kotlin package directory structure&quot;
  override fun getDescription(): String =
    &quot;Move Kotlin files to match idiomatic layout by omitting the root package according to the official recommendation.&quot;

  override fun getInitialValue(ctx: ExecutionContext) = AtomicReference&amp;lt;String?&amp;gt;(null) //4

  override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;() //5
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val computedPackage = acc.get()
        when (computedPackage) {
          null -&amp;gt; acc.set(packageName)                                         //6
          &quot;&quot; -&amp;gt; {}                                                             //7
          else -&amp;gt; {
            val commonPrefix = packageName.commonPrefixWith(computedPackage).removeSuffix(&quot;.&quot;) //8
            acc.set(commonPrefix)
          }
        }
        return cu
      }
    }
  }

  override fun getVisitor(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val packageToSet: String? = rootPackage ?: acc.get()                   //9
        if (packageToSet == null || packageToSet.isEmpty()) return cu
        val relativePath = packageName.removePrefix(packageToSet).removePrefix(&quot;.&quot;)
          .replace('.', '/')
        val filename = cu.sourcePath.fileName.toString()
        val newPath: Path = Paths.get(&quot;src/main/kotlin&quot;)
          .resolve(relativePath)
          .resolve(filename)
        return cu.withSourcePath(newPath)
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Inherit from &lt;code&gt;ScanningRecipe&lt;/code&gt; instead of directly from &lt;code&gt;Recipe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the accumulator type to be an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make the configuration easier when you don't override the package root&lt;/li&gt;
&lt;li&gt;The initial root is uninitialized&lt;/li&gt;
&lt;li&gt;Skip the computation if the root package is manually set&lt;/li&gt;
&lt;li&gt;If it's the first file visited, the accumulator holds &lt;code&gt;null&lt;/code&gt;, and we can set the (temporary) root as the current package&lt;/li&gt;
&lt;li&gt;One of the previous computations returned no common root–do nothing&lt;/li&gt;
&lt;li&gt;Find the longest common prefix between the held package root and the current package&lt;/li&gt;
&lt;li&gt;The only difference with the original code:&lt;br&gt;
we check if the root package has been set manually otherwise, we use the one computed in the first pass&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;optimizing-the-recipe&quot;&gt;Optimizing the recipe&lt;/h3&gt;
&lt;p&gt;You may have noticed that when there is no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;, we scan all files anyway. In a small codebase, it's not a big issue, but when scanning millions of source files, that's a huge waste of CPU cycles and time. If you run the recipe in the Cloud, it directly translates to money. We should stop scanning as soon as we detect the computed package root is an empty string to optimize the recipe.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
  if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()   //1
  val currentPackage = acc.get()
  if (currentPackage == &quot;&quot;) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()  //2
  return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
    override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
      val packageName = cu.packageDeclaration?.packageName ?: return cu
      // Different call than the one above!
      val currentPackage = acc.get()
      // First scanned file
      if (currentPackage == null) acc.set(packageName)                         //3
      else {
        // Find the longest common prefix between the stored package and the current one
        val commonPrefix = packageName.commonPrefixWith(currentPackage).removeSuffix(&quot;.&quot;)
        acc.set(commonPrefix)
      }
      return cu
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;If the root package has been set, skip visiting&lt;/li&gt;
&lt;li&gt;If one of the previous computations sets an empty string, there isn't any common root package: skip visiting&lt;/li&gt;
&lt;li&gt;Simplify the visitor by removing the clause when the accumulator is an empty string since it can't happen anymore&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that OpenRewrite still scans each file, but at least doesn't visit it thanks to the no-op visitor.&lt;/p&gt;
&lt;h3 id=&quot;counting-visits&quot;&gt;Counting visits&lt;/h3&gt;
&lt;p&gt;I made a couple of attempts before finding the right approach to the above. To ensure that I got it right, I wanted to display the number of visits by the scanner. We can use the accumulator to increment the visit count. Here are the changes I made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrated from an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt; to an &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;getInitialValue()&lt;/code&gt; function returns &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;(0 to null)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;During each visit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the visits count from the accumulator&lt;/li&gt;
&lt;li&gt;Increment it&lt;/li&gt;
&lt;li&gt;Print it&lt;/li&gt;
&lt;li&gt;Store it back in the accumulator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update the tests accordingly&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With packages &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt;, &lt;code&gt;org.frankel.blog.bar&lt;/code&gt;, and &lt;code&gt;org.frankel.blog.baz&lt;/code&gt;, the log shows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 file visited
[INFO] [stdout] FlattenStructure: 2 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By changing &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt; to &lt;code&gt;org.frankel.blog.foo&lt;/code&gt;, the log changes to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 files visited
[INFO] [stdout] FlattenStructure: 2 files visited
[INFO] [stdout] FlattenStructure: 3 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because I made these changes just to validate my understanding of how OpenRewrite works, I put them in the &lt;code&gt;visits_count&lt;/code&gt; branch on GitHub. To see the differences, execute &lt;code&gt;git diff master visits_count&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this post, I added the automatic computation of the root package. I had to change my design and understand how scanning recipes work. Then, I skipped further visits when there wasn't any common root package to optimize performance.&lt;/p&gt;
&lt;p&gt;The recipe is still not serializable, though it's a recommendation. I also noticed that my tests didn't leverage OpenRewrite's testing API. There's still a lot of work to do!&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/flatten-kotlin-recipe&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning Recipes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openrewrite/rewrite-testing-frameworks/blob/main/src/main/java/org/openrewrite/java/testing/mockito/AnyToNullable.java&quot;&gt;Code sample of an existing scanning recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/reference/scanning-recipes&quot;&gt;Existing scanning recipes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/2/&quot;&gt;A Java Geek&lt;/a&gt; on June 15&lt;sup&gt;th&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</guid>
<pubDate>Thu, 19 Jun 2025 09:26:47 +0000</pubDate>
</item>
<item>
<title>Koin on modularization: How to use it?</title>
<link>https://coderlegion.com/1504/koin-on-modularization-how-to-use-it</link>
<description>&lt;p&gt;&lt;strong&gt;What is Modularization in Android?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Modularization in Android means breaking an app into smaller, independent modules instead of keeping all code in a single project. Each module has its own responsibility and can be developed, tested, and maintained separately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic Prerequisites to Understand Modularization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before learning modularization, you should be familiar with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android Project Structure – Understanding how an app is built using&lt;br&gt;
 app/src/main. &lt;/li&gt;
&lt;li&gt;Gradle &amp;amp; Dependencies – Knowing how dependencies are&lt;br&gt;
 managed in build.gradle.kts. &lt;/li&gt;
&lt;li&gt;Packages &amp;amp; Code Organization – Basics&lt;br&gt;
 of organizing files using packages. &lt;/li&gt;
&lt;li&gt;Types of Android Modules – App&lt;br&gt;
 Module (Main application) &lt;/li&gt;
&lt;li&gt;Library Module (Reusability across&lt;br&gt;
 projects) &lt;/li&gt;
&lt;li&gt;Feature Module (For dynamic delivery in large apps)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why Modularization?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Faster Build Times – Only modified modules get compiled. &lt;/li&gt;
&lt;li&gt;✅ Better&lt;br&gt;
Code Maintainability – Easier to manage and scale. &lt;/li&gt;
&lt;li&gt;✅ Reusability –&lt;br&gt;
Common features like networking or authentication can be shared&lt;br&gt;
across projects. &lt;/li&gt;
&lt;li&gt;✅ Parallel Development – Teams can work on different&lt;br&gt;
modules independently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Today we are going to set up step by step how to use &lt;strong&gt;Koin&lt;/strong&gt; in modularization with &lt;strong&gt;viewModel&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Before we get started. Create some new modules and structured them like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8058861093745042862&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You have to determine what koin gradle version you want to use. For this tutorial, I use &lt;strong&gt;2.2.2&lt;/strong&gt; version. Define the gradle into di module&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation &quot;io.insert-koin:koin-android:2.2.2&quot;
implementation &quot;io.insert-koin:koin-androidx-viewmodel:2.2.2&quot;
implementation &quot;io.insert-koin:koin-androidx-scope:2.2.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step&lt;/strong&gt; 2&lt;/p&gt;
&lt;p&gt;After you have decided koin version you'd use, add other dependency your project would need for injecting to koin in di module as well. In this tutorial I want to inject Retrofit for network, AndroidX Room for local DB, Shared Preferences, ViewModel for managing data, and Coroutine for threading&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation &quot;io.insert-koin:koin-android:2.2.2&quot;
implementation &quot;io.insert-koin:koin-androidx-viewmodel:2.2.2&quot;
implementation &quot;io.insert-koin:koin-androidx-scope:2.2.2&quot;
implementation &quot;javax.inject:javax.inject:1&quot;

implementation &quot;com.squareup.okhttp3:logging-interceptor:4.9.0&quot;
implementation &quot;com.squareup.retrofit2:retrofit:2.9.0&quot;
implementation &quot;com.squareup.retrofit2:converter-moshi:2.9.0&quot;
implementation &quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;

implementation &quot;com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2&quot;

implementation &quot;androidx.room:room-runtime:2.4.2&quot;
kapt &quot;androidx.room:room-compiler:2.4.2&quot;
implementation &quot;androidx.room:room-ktx:2.4.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Define &lt;strong&gt;Network Module&lt;/strong&gt;, &lt;strong&gt;Api Module&lt;/strong&gt;, &lt;strong&gt;Shared Preferences Module&lt;/strong&gt;, &lt;strong&gt;Room Module&lt;/strong&gt;, and &lt;strong&gt;ViewModel Module&lt;/strong&gt; (we also use &lt;strong&gt;UseCase&lt;/strong&gt; and &lt;strong&gt;Repository&lt;/strong&gt; module for clean architecture principle)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ApiModule {
    companion object {
        val apiModule = module(override = true) {
            single { provideDisneyApi(get()) }
        }

        fun provideDisneyApi(retrofit: Retrofit): Api {
            return retrofit.create(Api::class.java)
        }
    }
}

class NetworkModule  {
    companion object {
        private fun httpInterceptor() = HttpLoggingInterceptor().apply {
            return HttpLoggingInterceptor { _ -&amp;gt;
            }.apply {
                level = HttpLoggingInterceptor.Level.BODY
            }
        }

        fun provideNetworkHandler(context: Context) = NetworkHandler(context)

        fun provideOkHttpClient() : OkHttpClient {
            return Builder.initInterceptor(httpInterceptor())
        }

        fun provideRetrofitService(okHttpClient: OkHttpClient): Retrofit {
            return Builder.initRetrofit(okHttpClient)
        }
    }
}

class SharedPrefModule {
    companion object {
        fun providePreference(context: Context) : SharedPreferences {
            return context.getSharedPreferences(&quot;SharedPreferenceName&quot;, Context.MODE_PRIVATE)
        }
    }
}

class RoomModule {
    companion object {
        fun provideRoom(context: Context) : DBConfig {
            return Room.databaseBuilder(context, DBConfig::class.java, &quot;db_sample&quot;)
                .fallbackToDestructiveMigration()
                .build()
        }
    }
}

class RepositoryModule {
    companion object {
        val repositoryModule = module(override = true) {
            single&amp;lt;CharacterRepository&amp;gt; { return@single CharacterRepositoryImpl(get(), get()) }
        }
    }
}

class UseCaseModule {
    companion object {
        val useCaseModule = module(override = true) {
            single&amp;lt;CharacterUseCase&amp;gt; { return@single CharacterUseCaseImpl(get()) }
        }
    }
}

class ViewModelModule {
    companion object {
        val viewModelModule = module(override = true) {
            viewModel { GamesListVM(get(), get()) }
            viewModel { GamesDetailVM(get(), get()) }
            viewModel { GamesFavoriteVM(get(), get(), get()) }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After you add them all, locate and categorize module that you prioritize as a 'core' module and wrap them into &lt;strong&gt;AppModule&lt;/strong&gt; class. In this tutorial I'll wrap &lt;strong&gt;retrofit&lt;/strong&gt;, &lt;strong&gt;room&lt;/strong&gt;, and &lt;strong&gt;shared preferences&lt;/strong&gt; module like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val MainAppModule = module(override = true) {
    single { NetworkModule.provideOkHttpClient() }
    single { NetworkModule.provideRetrofitService(get()) }
    single { NetworkModule.provideNetworkHandler(androidContext()) }
    single { RoomModule.provideRoom(androidContext()) }
    single { SharedPrefModule.providePreference(androidContext()) }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Call them with &lt;strong&gt;StartKoin&lt;/strong&gt; in &lt;strong&gt;Application Class&lt;/strong&gt; in &lt;strong&gt;App Module&lt;/strong&gt; (default module). I will call ViewModel, Repository, Usecase, and Api module as same level with MainApp Module&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DisneyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger(Level.ERROR)
            androidContext(this@DisneyApp)
            modules(MainAppModule, ApiModule.apiModule, RepositoryModule.repositoryModule,
                UseCaseModule.useCaseModule, ViewModelModule.viewModelModule
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And Voila! Congratulation you've built Koin in modular version. it's easy isn't it?&lt;/p&gt;
&lt;p&gt;Happy Coding :)&lt;/p&gt;
&lt;p&gt;Sample project: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mikkelofficial7/android-game-search&quot;&gt;Github link&lt;/a&gt;&lt;/p&gt;
</description>
<category>Best Practices</category>
<guid isPermaLink="true">https://coderlegion.com/1504/koin-on-modularization-how-to-use-it</guid>
<pubDate>Sat, 15 Mar 2025 20:05:51 +0000</pubDate>
</item>
</channel>
</rss>