<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged jetpackcompose</title>
<link>https://coderlegion.com/tag/jetpackcompose</link>
<description>Powered by Question2Answer</description>
<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>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>
</channel>
</rss>