[{"id":"gof-patterns-android","type":"blog","title":"8 GoF Patterns That Decide If Your Android App Scales","description":"Most mobile apps don't fail because of bad features — they fail because of bad architecture. Here are the 8 Gang of Four patterns that directly impact scale and reliability in production Android apps.","date":"2026-04-04T00:00:00.000Z","tags":["android","kotlin","architecture","design-patterns"],"authors":["me"],"url":"/blog/gof-patterns-android","content":"Most mobile apps don't fail because of bad features. They fail because of bad architecture. After 7+ years shipping Android apps — from solo projects to team codebases that onboard new devs every quarter — I keep coming back to the same 8 patterns from the Gang of Four book. Not because they're academic, but because they solve *real, recurring problems* at production scale. This is a practical series. No theory for its own sake — just the pattern, the Android problem it solves, and the Kotlin code that makes it click. --- ## The 8 Patterns at a Glance | Pattern | What it solves | Android context | |---|---|---| | **Observer** | UI that never goes stale | ViewModel + StateFlow | | **State** | Impossible states made impossible | Sealed `UiState` class | | **Proxy** | Cache + retry, invisible to callers | Repository layer | | **Facade** | One call hides 5 use cases | Feature API for ViewModels | | **Adapter** | Swap any SDK in one file | Analytics, payment SDKs | | **Factory** | Mockable sources from day one | Dependency injection | | **Strategy** | A/B test at runtime, zero rewrites | Feature flags | | **Decorator** | Add behaviours without touching core | Logging, auth, caching | --- ## The 4 Rules I Apply on Every Project These aren't guidelines — they're constraints that prevent the most common architectural mistakes: ``` → Every SDK gets an Adapter → Repository always = Proxy (cache gate) → One sealed UiState per screen → Facade per feature — keep ViewModels thin ``` Breaking any of these is fine in a prototype. In production, each one eventually costs you. --- ## The Numbers at Scale When these patterns are applied consistently across a codebase: - **3× faster team onboarding** — new devs find predictable structure everywhere - **70% less boilerplate** — patterns eliminate repeated decision-making - **0 SDK lock-in** with Adapter — I've swapped analytics SDKs in a single afternoon - **10× faster unit tests** — Factory + Adapter means no real network, no real disk --- ## Series Structure Each part covers two patterns — their relationship, their code, and how they interact: 1. **[Observer & State](./observer-state)** — UI reactivity and compiler-enforced screen states 2. **[Proxy & Facade](./proxy-facade)** — Cache gates and thin ViewModels 3. **[Adapter & Factory](./adapter-factory)** — SDK independence and testable sources 4. **[Strategy & Decorator](./strategy-decorator)** — Runtime behavior and additive extensions --- Architecture is the decision you make at 9am that saves your team at 2am. Start with Part 1: [Observer & State →](./observer-state)"},{"id":"pretext","type":"blog","title":"Pretext: The 15 kb Library That Bypasses Your Browser's Most Expensive Operation","description":"Every time your JavaScript measures text with getBoundingClientRect(), the browser discards its entire layout tree and recalculates from scratch. Pretext eliminates that cost using canvas arithmetic — and the results are dramatic.","date":"2026-04-04T00:00:00.000Z","tags":["javascript","performance","web","typography"],"authors":["me"],"url":"/blog/pretext","content":"Every time you call `getBoundingClientRect()` to measure a text element, your browser quietly does something brutal: it discards its entire layout tree, recalculates every position from scratch, and hands you back a number. This happens synchronously, on the main thread, and it blocks everything else. For a static blog this doesn't matter. For a streaming AI chat interface updating 60 times per second — or a virtualized list with hundreds of variable-height items — it is a wall. **Pretext** is a 15 kb library by [Cheng Lou](https://github.com/chenglou) (creator of React Motion, senior engineer at Midjourney) that eliminates this cost entirely. It measures and lays out multiline text using pure arithmetic, never touching the DOM after preparation. --- ## The Core Idea in One Paragraph Pretext splits work into two phases. `prepare()` runs once per text+font combination: it uses Canvas's `measureText()` API to measure every text segment, then caches the results. `layout()` runs as many times as you need: it computes line breaks and heights using only arithmetic on those cached widths — zero DOM reads, zero reflow. Width changes are free. Streaming updates are free. You pay the measurement cost once. ```ts import { prepare, layout } from '@chenglou/pretext' const prepared = prepare('Layout reflow is the silent performance killer.', '16px Inter') // First width — cheap const { height, lineCount } = layout(prepared, 320, 24) // Different width — still just arithmetic const { height: h2 } = layout(prepared, 480, 24) ``` --- ## Why It Matters Now The timing is not accidental. AI applications that stream text token by token need to resize bubbles on every frame. Virtualized lists need to predict heights before elements exist in the DOM. Masonry layouts need to know heights before placing cards. All of these patterns were either janky or required workarounds involving hidden off-screen containers. Pretext solves the underlying problem rather than working around it: **treat the browser's font engine as an oracle during preparation, then never ask it again**. --- ## The Numbers | Operation | Cost | |---|---| | `prepare()` for 500 blocks | ~19 ms (same as one DOM pass) | | `layout()` per call | ~0.09 ms | | `getBoundingClientRect()` per call | ~0.04 ms — but forces reflow when layout is dirty | | Full reflow on dirty tree | 10–100+ ms depending on page complexity | The real win is not the per-call speed — it is that `layout()` never dirties the layout tree, so it never triggers the cascade. --- ## Series Structure This series covers pretext from fundamentals to live demos: 1. **[The Reflow Tax](/blog/pretext/reflow-tax)** — what layout reflow actually is, when it hurts, and why `getBoundingClientRect()` is expensive 2. **[How Pretext Works](/blog/pretext/how-it-works)** — the two-phase model, Canvas oracle, arithmetic layout, and the full API 3. **[React Demo: Streaming Chat](/blog/pretext/react-demo)** — building a live streaming AI chat with pretext-measured bubble heights 4. **[Matteflow: Text Around a Dancer](/blog/pretext/matteflow)** — a full editorial demo: text flowing around a moving foreground subject, recomputed every frame with pretext --- Live demo Skip the theory — see it move Text flowing around a dancing figure, recomputed every frame with pretext. No setup, no install. Open Matteflow Demo → Start with [The Reflow Tax →](/blog/pretext/reflow-tax)"},{"id":"compose-remote","type":"blog","title":"Remote Compose: Server-Driven UI Comes to Android the Right Way","description":"Remote Compose (androidx.compose.remote) lets you define Compose UI on a server and render it on any Android device without a Play Store update. Here's how it works, why it matters, and how it compares to existing approaches.","date":"2026-04-01T00:00:00.000Z","tags":["android","kotlin","compose","jetpack"],"authors":["me"],"url":"/blog/compose-remote","content":"Something quietly significant landed in Jetpack at the end of 2025. `androidx.compose.remote` — **Remote Compose** — is Google's first-party answer to server-driven UI on Android. It reached alpha07 on March 25, 2026, and it's worth understanding what it is and, more importantly, what problem it actually solves. ## The Problem That Never Got Solved Server-Driven UI (SDUI) has been a known pattern in Android for years. Airbnb, Twitter, Lyft, and others have built their own SDUI stacks. The common approach: define a JSON schema for components, ship a renderer that maps JSON keys to actual UI, and update the UI by changing the server payload. It works. But it has real downsides: - **JSON is verbose and untyped** — you build your own contract and hope both sides agree - **Renderers diverge** — the client renderer and the server definition drift over time - **No compile-time safety** — a typo in a component name silently renders nothing Meanwhile, Android's built-in solution was `RemoteViews` — a serializable, cross-process UI format that's been around since Android 1.5. Glance widgets, Wear OS tiles, lock screen widgets — they all use `RemoteViews` today. It's reliable but deeply limited: no custom layouts, no arbitrary composables, no Compose at all. Remote Compose is the successor to both. --- ## What Remote Compose Actually Is Remote Compose is a **serialization layer on top of Jetpack Compose semantics**. Instead of rendering to a Canvas, you write Compose-like code using the `remote-creation` API, and it produces a compact binary **document** — a serialized UI tree. That document is then transmitted (over the network, IPC, or however you want) to an Android client that renders it using the `remote-player` runtime. The key insight: **the client doesn't need a Compose compiler or a Kotlin runtime for the UI code**. The player just deserializes the document and paints it. No APK update required. ``` Server / JVM Process └── remote-creation API → writes Compose-like code ↓ produces [ binary document ] ↓ transmitted Android Client └── remote-player → deserializes + renders to View/Compose host ``` ### How It Differs from JSON-based SDUI | | JSON SDUI | Remote Compose | |---|---|---| | Format | Text (verbose) | Binary (compact) | | Type safety | None (runtime) | Enforced at creation time | | Renderer | Custom, diverges | System-provided, versioned | | Layout power | Limited by your schema | Full Compose layout model | | First-party | No | Yes (Google/Jetpack) | --- ## The Glance Connection If you've used Glance (the Jetpack library for widgets and Wear OS tiles), you're using `RemoteViews` under the hood — the same API from 2009. Remote Compose is **Glance's future rendering backend**. The migration is already in progress. This context matters: Remote Compose isn't an experimental curiosity. It's the infrastructure Google is building for the next decade of widgets and cross-process UIs. The \"Back to the Future\" framing that [Arman Chatikyan](https://medium.com/@chatikyan/remote-compose-back-to-the-future-454b8e824fad) used is apt: same problem as RemoteViews in 2009, modern solution. --- ## Module Structure The library is split into **creation** (server/JVM side) and **player** (Android client side): ```kotlin // Server / JVM — produces documents implementation(\"androidx.compose.remote:remote-creation:1.0.0-alpha07\") implementation(\"androidx.compose.remote:remote-creation-compose:1.0.0-alpha07\") // if writing from Compose // Android client — renders documents implementation(\"androidx.compose.remote:remote-player-core:1.0.0-alpha07\") implementation(\"androidx.compose.remote:remote-player-view:1.0.0-alpha07\") // Tooling debugImplementation(\"androidx.compose.remote:remote-tooling-preview:1.0.0-alpha07\") ``` The separation is intentional: the creation side can run on a JVM backend (a Spring service, a Cloud Function, etc.) with no Android dependency. The player side is Android-only. --- ## The RemoteApplier Boundary One design decision that stands out: since alpha04, the `RemoteApplier` is **enabled by default**. This is a compile-time guard that prevents regular Jetpack Compose composables from being accidentally used inside Remote Compose code. In practice, Remote Compose functions look similar to regular Compose functions, but they're in a different composition scope. The compiler enforces the boundary. If you try to call a regular `@Composable` from inside a Remote Compose function, you get a compile-time error. This is the right call — it prevents a class of bugs where non-serializable UI leaks into a document that the player can't render. --- ## What's Available in alpha07 (March 25, 2026) The latest release added: - **Non-linear font scaling** — respects accessibility settings for large text - **LayoutDirection** — RTL/LTR support for international layouts - `RemoteSpacer`, `RemoteImageVector`, `painterRemoteVector` — newly public APIs - Semantic modifier functions — accessibility support - `RemoteArrangement.spacedBy()` — gap-based layout spacing - Extended `RemoteDrawScope`, `RemoteCanvas`, `RemotePainter`, `RemoteBrush` - `RemoteFloat` arithmetic operations and `asRemoteDp()` conversion **Breaking changes in alpha07:** - `RemoteArrangement.CenterHorizontally` removed — use `RemoteArrangement.Center` - `RemoteBox` alignment parameter changed to a single `RemoteAlignment` --- ## Current State: Still Alpha, But Moving Fast The library has shipped 7 alpha releases in roughly 3.5 months (December 2025 → March 2026). The pace is aggressive: each release both adds new public APIs and breaks existing ones. Milestone by milestone, the API surface has grown from almost nothing to a reasonably complete set of layout primitives. But the fact that every release still has breaking changes means this isn't production-ready — yet. Meaningful milestones so far: - **alpha04**: `minSdk` dropped from 26 to 23 — a signal the team intends broad adoption - **alpha04**: `FlowLayout` support added - **alpha06**: Java 11 target (may require desugaring) - **alpha07**: Font scaling + RTL — table stakes for internationalized apps --- ## Should You Use It Now? **For production apps**: not yet. The API breaks every two weeks, and the player runtime's capabilities are still being defined. **For Glance widget developers**: start paying attention. This is where Glance is going, and early familiarity will pay off when the migration path is formalized. **For SDUI teams**: worth a serious prototype. If your team has been maintaining a custom SDUI stack, the appeal of a first-party, type-safe, binary-format alternative is obvious. **For widget/tile use cases**: this is arguably the most immediate practical application, since Glance is already on this path. --- ## Next Up In the [next part](./getting-started), we'll walk through a practical example: building a simple Remote Compose document on the creation side, serializing it, and rendering it inside an Android app using `remote-player-view`. --- ## Resources - [Remote Compose — Official Release Notes](https://developer.android.com/jetpack/androidx/releases/compose-remote) - [Arman Chatikyan — Remote Compose: Back to the Future](https://medium.com/@chatikyan/remote-compose-back-to-the-future-454b8e824fad) - [Luca Fioravanti — From RemoteViews to RemoteCompose](https://medium.com/@fioravanti.luka/glance-remoteviews-and-remotecompose-what-actually-changed-in-android-16-4afc4b63b0ad) - [Nativeblocks — Google's Official Answer to SDUI](https://nativeblocks.io/blog/remote-compose-googles-answer-to-server-driven-ui)"},{"id":"android-17-beta-3","type":"blog","title":"Android 17 Beta 3: Platform Stability Reached — What Developers Need to Know","description":"Android 17 Beta 3 (API level 37) marks platform stability. APIs are locked, apps can now target Android 17 on Google Play. Here's a full breakdown of every new API, breaking change, and what to do right now.","date":"2026-03-27T00:00:00.000Z","tags":["android","kotlin","android-17"],"authors":["me"],"url":"/blog/android-17-beta-3","content":"Android 17 Beta 3 dropped on March 26, 2026, and it carries a very specific signal for developers: **platform stability has been reached**. That means the API surface is now locked. No more additions, no more removals, no more surprises. If you're a library author, SDK vendor, or app developer, the clock is ticking. Build number `CP21.260306.017`, API level 37, security patch `2026-03-05`. This is the real thing. ## What \"Platform Stability\" Means in Practice Google uses Beta 3 as the point at which the final API set is frozen. From here until the stable release: - **No new public APIs will be added or removed** - Apps submitted to Google Play can now target API 37 - SDK and library authors should publish compatibility updates immediately - Game engines and developer tooling vendors need to validate against this build The stable release is expected later in 2026. We don't have an exact date yet, but the timeline mirrors previous years: Beta 3 in March → stable around Q3. --- ## Camera and Media ### Photo Picker Gets Aspect Ratio Customization A long-requested feature: the photo picker now supports portrait `9:16` grid aspect ratio alongside the existing `1:1` square. Use `PhotoPickerUiCustomizationParams`: ```kotlin val params = PhotoPickerUiCustomizationParams.Builder() .setAspectRatio(PhotoPickerUiCustomizationParams.ASPECT_RATIO_PORTRAIT_9_16) .build() ``` Works with `ACTION_PICK_IMAGES` and the embedded photo picker variant. Useful for apps where vertical video or portrait photography is the primary use case. ### RAW14: 14-bit Single-Channel RAW Format `ImageFormat.RAW14` is a new constant for 14-bit per pixel, densely packed RAW images (4 pixels stored in every 7 bytes). Professional camera apps with access to compatible sensors can now capture maximum color depth without custom format hacks. ### Camera Device Type Queries You can now query whether a camera is built-in hardware, an external USB webcam, or a virtual camera. No more guessing from `CameraCharacteristics` workarounds. ### Vendor-Defined Camera Extensions Hardware partners can define custom extension modes — think Super Resolution, AI-driven night modes, or manufacturer-specific enhancements. Query support via `isExtensionSupported(int)` on `CameraExtensionCharacteristics`. ### Bluetooth LE Audio Hearing Aid Support New `AudioDeviceInfo.TYPE_BLE_HEARING_AID` constant distinguishes hearing aids from generic LE Audio headsets: ```kotlin val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) val hearingAidConnected = devices.any { it.type == AudioDeviceInfo.TYPE_BLE_HEARING_AID } ``` This also enables **granular hearing aid audio routing** — users can independently route notifications, ringtones, and alarms to the hearing aid or device speaker, with no app changes required. ### Extended HE-AAC Software Encoder New system-provided MediaCodec (`c2.android.xheaac.encoder`) for high-efficiency speech and audio encoding. Unified codec supporting both high and low bitrates, with mandatory loudness metadata for consistent volume in low-bandwidth conditions. ```kotlin val encoder = MediaCodec.createByCodecName(\"c2.android.xheaac.encoder\") val format = MediaFormat.createAudioFormat( MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate = 48000, channelCount = 1 ) format.setInteger( MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectXHE ) ``` --- ## Privacy and Security ### System-Provided Location Button A new system-rendered button (delivered via Jetpack) that grants **one-time precise location** for the current session only — with no system permission dialog. Requires the new `USE_LOCATION_BUTTON` permission. This is a meaningful UX improvement for apps where users want to share their location for a single action (ordering food, sharing with a friend) without granting persistent location access. ### Discrete Password Visibility The \"Show passwords\" setting is now split in two: - **Touch/soft keyboard input**: briefly echoes the last typed character (same as before) - **Physical keyboard input**: hidden immediately by default If your app uses custom text fields, use the new `ShowSecretsSetting` API: ```kotlin val isPhysicalKeyboard = event.source and InputDevice.SOURCE_KEYBOARD == InputDevice.SOURCE_KEYBOARD val shouldShowPassword = android.text.ShowSecretsSetting.shouldShowPassword(context, isPhysicalKeyboard) ``` ### Post-Quantum Cryptography APK Signing Android 17 introduces **APK signature scheme v3.2** with a hybrid approach: classical RSA or Elliptic Curve signatures combined with ML-DSA (Module Lattice Digital Signature Algorithm — a NIST-standardized post-quantum algorithm). This future-proofs the ecosystem against quantum computing threats without breaking compatibility with current tooling. ### Native Dynamic Code Loading — Read-Only Enforcement Extended Since Android 14, Java/DEX files loaded dynamically must be read-only. **Android 17 extends this enforcement to native libraries**. If your app calls `System.load()` on a native file that isn't marked read-only, it now throws `UnsatisfiedLinkError`. Audit your native library loading paths. ### Certificate Transparency — On by Default What was an opt-in feature on Android 16 is now **enabled by default for all apps**. No code changes needed for apps that use standard HTTPS — but if you're doing manual certificate validation or certificate pinning, review your implementation. --- ## Performance and Battery ### AlarmManager: Exact Alarms with a Listener Callback New overload of `setExactAndAllowWhileIdle()` that accepts an `OnAlarmListener` instead of a `PendingIntent`. Intended for apps currently using continuous wakelocks for precise timing (messaging sockets, medical monitoring apps): ```java alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, triggerAtMillis, \"com.example.MY_ALARM\", executor, new AlarmManager.OnAlarmListener() { @Override public void onAlarm() { // Handle the alarm } } ); ``` > **Note:** CommonsWare flagged the API documentation as internally contradictory — it simultaneously claims to target apps \"reliant on continuous wakelocks\" but also \"reduces wakelocks.\" The intent appears to be a more targeted alternative to long-lived wakelocks, not a complete removal of wakelock semantics. --- ## User Experience and System UI ### Widget Support on External Displays `RemoteViews.setViewPadding()` now accepts complex units (DP and SP, not just pixels). Widgets can retrieve the display ID of the screen they're rendering on via `OPTION_APPWIDGET_DISPLAY_ID`: ```kotlin val displayId = appWidgetManager .getAppWidgetOptions(appWidgetId) .getInt(AppWidgetManager.OPTION_APPWIDGET_DISPLAY_ID) ``` This makes it possible to adapt widget layouts for external monitors with different pixel densities — critical for Desktop Mode. ### Desktop Interactive Picture-in-Picture Apps can request a pinned windowing layer in desktop mode (now the default on external displays). The window is always-on-top and fully interactive. Requires the new `USE_PINNED_WINDOWING_LAYER` permission: ```kotlin appTask.requestWindowingLayer( ActivityManager.AppTask.WINDOWING_LAYER_PINNED, context.mainExecutor, object : OutcomeReceiver { override fun onResult(result: Int) { /* success */ } override fun onError(e: Exception) { /* handle */ } } ) ``` ### Hidden App Labels Users can now hide app names on the home screen. Google's guidance to developers: make sure your app icon is recognizable without its label. ### Redesigned Screen Recording Toolbar New floating toolbar for recording controls. The toolbar itself is excluded from the final captured video — a simple but important fix that any screen recorder user will appreciate. --- ## Breaking Changes ### For All Apps (Regardless of Target API) | Change | What to do | |---|---| | **DCL native enforcement** | Ensure native libs loaded via `System.load()` are on read-only paths | | **Certificate Transparency on by default** | Review custom certificate validation code | | **Local Network Access blocked** | Apps targeting Android 17+ need `ACCESS_LOCAL_NETWORK` permission to reach LAN devices | ### For Apps Targeting API 37 | Change | What to do | |---|---| | **Large screen resizability enforced** | Cannot opt out of orientation/resizability constraints on large screens — test on foldables and tablets | | **`String.getChars()` removed** | Migrate to `String.getBytes()` or equivalent — this is an OpenJDK 21 change | | **`ACTION_TAG_DISCOVERED` deprecated** | Migrate to the newer NFC intent actions | | **`DnsResolver.getInstance()` removed** | Use the constructor-based approach instead | --- ## Notable Underdocumented APIs CommonsWare identified several new APIs in the API diff that lack documentation: - **`SerialManager`** — serial port access, scope unclear - **`WebAppManager`** — relationship to the browser unknown - **`FileManager`** system service — background disk I/O for privileged apps - **Bridged notifications** — from other connected devices - **ANR warning registration** — graceful timeout callbacks before an ANR is triggered - **Alternative SMS transport framework** — RCS and beyond Treat these as internal or partner-restricted for now. --- ## Stability Fixes in Beta 3 Beta 3 included an unusually large number of stability fixes, suggesting that earlier betas had significant reliability issues: - **Process lifecycle regression** (Android 16) causing random app restarts and screen flickering - **Camera**: 5x telephoto lens switching failures, lens transition stuttering - **Android Auto**: lock screen freeze after disconnection - **Spontaneous reboots**: 40+ related issues, hangs during idle - **Bluetooth**: 150-second pairing hang - **UI artifacts**: status bar icons disappearing, notification dismiss failures --- ## What You Should Do Right Now 1. **Library/SDK authors**: publish your Android 17 compatibility release now. The API surface is frozen. 2. **App developers**: run your app on the Beta 3 system image or emulator. Pay attention to the native DCL enforcement and local network access changes. 3. **Test on large screens**: the resizability enforcement change is the most likely to surface new issues in foldable/tablet form factors. 4. **Check your NFC code**: if you use `ACTION_TAG_DISCOVERED`, start planning the migration. 5. **Submit your app to Google Play** targeting API 37 — you can do it now. --- ## Resources - [Android 17 Beta 3 — Official Blog Post](https://android-developers.googleblog.com/2026/03/the-third-beta-of-android-17.html) - [Android 17 Release Notes](https://developer.android.com/about/versions/17/release-notes) - [Android 17 API Differences Report](https://developer.android.com/sdk/api_diff/37/changes) - [CommonsWare — Beta 3 Musings](https://commonsware.com/blog/2026/03/27/random-musings-android-17-beta-3.html)"},{"id":"keyboard-simple","type":"project","title":"Keyboard Simple","description":"A lightweight Android keyboard library distributed via JitPack for easy integration into any Android project.","date":"2023-05-23T00:00:00.000Z","tags":["Android","Java","Kotlin","Library"],"authors":[],"url":"/projects/keyboard-simple","content":"import Callout from '@/components/Callout.astro' **Keyboard Simple** is a lightweight Android keyboard library built with Java and Kotlin, designed to be dropped into any Android project with minimal configuration. It is distributed via [JitPack](https://jitpack.io) and covers the most common keyboard customization needs without pulling in heavy dependencies. ## Integration ### 1. Add JitPack to your repositories ```kotlin // settings.gradle.kts (or build.gradle) dependencyResolutionManagement { repositories { maven { url = uri(\"https://jitpack.io\") } } } ``` ### 2. Add the dependency ```kotlin // build.gradle.kts (app module) dependencies { implementation(\"com.github.NearApps:Keyboard-Simple:1.0.5\") } ``` The latest published release is **v1.0.5**. You can find all releases on the [GitHub releases page](https://github.com/NullKDev/Keyboard-Simple/releases). ## Tech Stack - **Java** — core library implementation (70%) - **Kotlin** — modern helpers and extensions (30%) - **Apache 2.0** — open-source, free to use in commercial projects"}]