bilíngue · en / pt
check-in binário
The Plan already has a post here, with the why, the deadline and the hour-cost. This one is about the tool: the Android app I built to track that every day, why it is the way it is, and an idea left over at the end, when I was already inside Kotlin and didn't want to leave.
why a spreadsheet doesn't hold this
$ quote ~/posts/o-planoThe Plan2026-09-09enI didn't build this to show anyone. I built it because a spreadsheet doesn't hold that kind of discipline, and I needed something that would stare back at me every day with the real number.
The problem with a spreadsheet isn't that it's too simple. It's that it's too obedient. It accepts any number I type into it, including the ones I type in a bad mood at eleven at night to close the week somewhere respectable. I had done that before and knew exactly how it ends: the spreadsheet stays beautiful and the studying didn't happen.
What I needed was something that wouldn't negotiate. 492 hours until June 30, 2027, spread across 32 credentials. That number isn't motivational, it's arithmetic, and the app's only job is to keep it visible every day without letting me touch it.
I studied or I didn't
The check-in is binary. I studied on Coursera today, or I didn't. No timer, no duration field, no notes, no partial progress.
That was the first decision and it holds up all the rest. The minute a duration field exists, it becomes a negotiation: forty distracted minutes of video at 1.75x become "an hour," and an hour like that disappears into three hundred others. Binary has nothing to negotiate. Either the day happened or it didn't.
The second decision was the window: I can mark and unmark the current day and the six before it, and that's all. Seven days is roughly how far back I can remember honestly. Past that it becomes reconstruction, and reconstructed memory is always generous to whoever is reconstructing.
the Wall
The screen I use most is called the Wall, and it answers one question: am I behind?
It shows how many hours are left, the theoretical pace to finish within the deadline, a traffic light, and a staircase burn-down. The math itself is nothing clever:
val restantes = totalPlanejado - concluidas - cortadas
val semanas = ChronoUnit.WEEKS.between(hoje, FIM)
val ritmo = restantes / max(1, semanas)What it has going for it is that I can't edit it. And the chart is a staircase, not a ramp, because real progress is discrete: the Wall's hours only drop when a course is completed. A check-in drops nothing. Thirty days in a row with zero courses closed shows up as exactly what it is, one long horizontal line, and it's uncomfortable in the right way.
There's a valve, and it's declared: grupo_corte, in the database. I can cut a planned item and its hours leave the Wall. That isn't cheating, it's the same decision I already made by hand when boot.dev left the main track because of the deadline. The difference is that now a cut stays on record as a cut, instead of becoming a goal that quietly shrank.
what's underneath
Kotlin, Jetpack Compose, Material 3, Navigation 3. Room underneath, Hilt for injection, StateFlow for state, WorkManager for the reminder. minSdk 26, targetSdk 36, package dev.nuxyel.oplano, MIT, code on GitHub.
The database, o_plano.db, has seven tables: trilha, ordem, grupo_corte, item, selo, checkin and config. The modeling mirrors The Plan's structure instead of inventing some generic habit abstraction, and that was deliberate: the app is good for nothing else, it doesn't need to pretend otherwise.
The only part that gave me real trouble was the Room migration preserving the v1.0 data. It's a personal app, with one user, and even so I couldn't drop the table and start over, because the history is the product. The value in there isn't the code, it's the row of check-ins since July. Losing that to reorganize a schema would be losing the only thing the app has.
There are four screens: Today (the current item, the seven editable days, the streak), the Wall, Tracks (with cut and restore) and Credentials, which is a timeline of the certificates. Plus the item detail, which is where the boring part lives: planned load, dates, state, completion.
what I didn't build
No login, no backend, no telemetry, no sync, no network access. No export, no import, Android's automatic backup switched off.
The data exists only on the current installation. If I lose the phone, I lose the whole history, and I decided to accept that consciously instead of solving it with a Firebase that would cost me an afternoon and a permanent attachment. The app is for me, it runs on my phone, and its border is the glass.
the idea it left behind
Once the app was done, something I didn't expect was left over: wanting to stay in Kotlin.
It isn't about Android. It's that I hadn't written in a typed, compiled, boring-in-a-good-way language for a while, after months of Python, and that stirred up a question now parked in my head as a possible thesis: can you do machine learning and computer vision in native Kotlin, with no external library? No TFLite, no ML Kit, no OpenCV, nothing calling into C++ underneath. Convolution written by hand, feature extraction written by hand, all on the JVM.
I haven't researched whether that's viable. Not at all. I don't know if it's a thesis or an afternoon of frustration with a benchmark thirty times slower than the baseline and no interesting conclusion at the end. I don't know if the literature already exists, I don't know where the JVM breaks on real matrix work, I don't know if the right framing is implement it from scratch or run it on-device without depending on a third party. Those are different questions and they make different theses.
What I do know is why the idea pulls at me, and it's the same reason as always: it's fundamentals, not tooling. Calling model.predict() doesn't teach me what convolution does. Writing the convolution does. If the answer at the end is "it's unviable and here's why," that's a result too, and it's a result I would understand end to end.
It stays on record as what it is: a raw idea, viability unchecked, written down the day it showed up. In a few months I'll come back here and see whether it survived contact with reality.
O Plano já tem um post aqui, com o porquê, o prazo e o preço em horas. Esse é sobre a ferramenta: o app Android que eu construí pra acompanhar aquilo todo dia, por que ele é do jeito que é, e uma ideia que sobrou no fim, quando eu já estava dentro do Kotlin e não queria mais sair.
por que planilha não segura isso
$ quote ~/posts/o-planoO Plano2026-09-09ptNão construí isso pra mostrar pra ninguém. Construí porque planilha não segura esse tipo de disciplina, e eu precisava de alguma coisa que me encarasse todo dia com o número real.
O problema da planilha não é ela ser simples demais. É ela ser obediente demais. Ela aceita qualquer número que eu digitar, inclusive os que eu digito de mau humor às onze da noite pra fechar a semana num lugar decente. Eu já tinha feito isso antes e sabia exatamente como termina: a planilha continua linda e o estudo não aconteceu.
O que eu precisava era de uma coisa que não negociasse. 492 horas até 30 de junho de 2027, espalhadas em 32 selos. Esse número não é motivacional, é aritmético, e a única função do app é deixar ele visível todo dia sem me deixar mexer nele.
estudei ou não estudei
O check-in é binário. Estudei na Coursera hoje, ou não estudei. Sem cronômetro, sem campo de duração, sem anotação, sem progresso parcial.
Isso foi a primeira decisão e é a que sustenta o resto. No minuto em que existe um campo de duração, ele vira negociação: quarenta minutos distraído com vídeo em 1.75x viram "uma hora", e uma hora dessas some no meio de outras trezentas. Binário não tem o que negociar. Ou o dia aconteceu ou não aconteceu.
A segunda decisão foi a janela: dá pra marcar e desmarcar o dia atual e os seis anteriores, e só. Sete dias é mais ou menos o quanto eu consigo lembrar com honestidade. Passou disso, virou reconstrução, e reconstrução de memória sempre sai generosa com quem reconstrói.
o Muro
A tela que eu mais uso chama Muro, e ela responde uma pergunta só: eu tô atrasado?
Ela mostra quanto falta das horas, o ritmo teórico pra fechar dentro do prazo, um semáforo, e um burn-down em escada. A conta em si não tem nada de esperto:
val restantes = totalPlanejado - concluidas - cortadas
val semanas = ChronoUnit.WEEKS.between(hoje, FIM)
val ritmo = restantes / max(1, semanas)O que ela tem é de não poder ser editada por mim. E o gráfico é escada, não rampa, porque o progresso real é discreto: as horas do Muro só caem quando um curso é concluído. Check-in não derruba hora nenhuma. Sequência de trinta dias seguidos com zero curso fechado aparece exatamente como o que é, uma linha horizontal comprida, e é desconfortável do jeito certo.
Tem uma válvula, e ela é declarada: grupo_corte, no banco. Dá pra cortar um item planejado e as horas dele saem do Muro. Isso não é trapaça, é a mesma decisão que eu já tinha tomado na mão quando o boot.dev saiu da trilha principal por causa do prazo. A diferença é que agora o corte fica registrado como corte, em vez de virar uma meta que silenciosamente encolheu.
o que tem por baixo
Kotlin, Jetpack Compose, Material 3, Navigation 3. Room por baixo, Hilt pra injeção, StateFlow pro estado, WorkManager pro lembrete. minSdk 26, targetSdk 36, package dev.nuxyel.oplano, MIT, código no GitHub.
O banco, o_plano.db, tem sete tabelas: trilha, ordem, grupo_corte, item, selo, checkin e config. A modelagem espelha a estrutura d'O Plano em vez de inventar uma abstração genérica de hábito, e isso foi de propósito: o app não serve pra mais nada, não precisa fingir que serve.
A única parte que me deu trabalho de verdade foi a migração do Room preservando os dados da v1.0. É um app pessoal, com um usuário, e mesmo assim eu não podia dropar a tabela e recomeçar, porque o histórico é o produto. O valor daquilo ali não é o código, é a fileira de check-ins desde julho. Perder isso pra reorganizar schema seria perder a única coisa que o app tem.
As telas são quatro: Hoje (o item da vez, os sete dias editáveis, a sequência), Muro, Trilhas (com corte e restauro) e Selos, que é uma linha do tempo das credenciais. Mais o detalhe do item, que é onde mora a parte chata: carga planejada, datas, estado, conclusão.
o que eu não construí
Sem login, sem backend, sem telemetria, sem sincronização, sem acesso à rede. Sem export, sem import, backup automático do Android desligado.
Os dados existem só na instalação atual. Se eu perder o aparelho, perco o histórico inteiro, e eu decidi aceitar isso conscientemente em vez de resolver com um Firebase que me custaria uma tarde e um vínculo permanente. O app é pra mim, roda no meu aparelho, e a fronteira dele é o vidro.
a ideia que sobrou
Terminado o app, sobrou uma coisa que eu não esperava: vontade de continuar em Kotlin.
Não é sobre Android. É que fazia tempo que eu não escrevia numa linguagem tipada, compilada e chata no bom sentido, depois de meses em Python, e isso mexeu com uma pergunta que agora tá parada na minha cabeça como possível TCC: dá pra fazer machine learning e visão computacional em Kotlin nativo, sem lib externa? Sem TFLite, sem ML Kit, sem OpenCV, sem chamar nada em C++ por baixo. A convolução escrita na mão, a extração de features escrita na mão, tudo na JVM.
Eu não pesquisei a viabilidade disso. Nem um pouco. Não sei se é um TCC ou se é uma tarde de frustração com um benchmark trinta vezes mais lento que o baseline e nenhuma conclusão interessante no fim. Não sei se já existe literatura, não sei onde a JVM quebra em operação de matriz de verdade, não sei se o recorte certo é implementar do zero ou rodar on-device sem depender de terceiro. São perguntas diferentes e elas dão TCCs diferentes.
O que eu sei é por que a ideia me atrai, e é o mesmo motivo de sempre: é fundamento, não ferramenta. Chamar model.predict() não me ensina o que a convolução faz. Escrever a convolução ensina. Se no fim a resposta for "é inviável e aqui está o motivo", isso também é um resultado, e é um resultado que eu ia entender de ponta a ponta.
Fica registrado como o que é: uma ideia crua, com viabilidade não checada, anotada no dia em que ela apareceu. Daqui uns meses eu volto aqui e vejo se ela sobreviveu ao contato com a realidade.