Detailed reference for the core composables and configuration types in the
markdownmodule.
- For an overview, installation and feature tour, see README.md.
Synchronous parsing version. Parses the Markdown string inside a remember block on the UI thread.
Use this for small or medium Markdown documents where parsing time is negligible.
Signature (from MarkdownView.kt):
@Composable
fun MarkdownView(
content: String,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
onError: (@Composable (Throwable) -> Unit)? = null,
)Parameters
content: Markdown text to render.markdownRenderConfig: The rendering configuration, created viaMarkdownRenderConfig.Builder().modifier: Standard Compose modifier for sizing, padding, etc.showNotSupportedText: Iftrue, unsupported elements are rendered as a textual fallback instead of being silently ignored.actionHandler: Optional handler for clickable actions (e.g. links, images, custom blocks). See project examples for concrete implementations.onError: Optional composable invoked when parsing fails. Whennull, errors are swallowed (nothing is rendered).
Notes
- Parsing is done immediately during composition; avoid using this overload for very large strings.
- If you need loading / error UI, prefer the async overload below.
Asynchronous parsing version. Offloads parsing to a background dispatcher and exposes loading / error state hooks.
Recommended for large content or when you want a loading indicator and error UI.
Signature (from MarkdownView.kt):
@Composable
fun MarkdownView(
content: String,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
parseDispatcher: CoroutineDispatcher? = null,
onLoading: (@Composable () -> Unit)? = null,
onError: (@Composable (Throwable) -> Unit)? = null,
)Additional Parameters
parseDispatcher: Dispatcher used for parsing. Whennull, the library usesMarkdownThreadPool.dispatcher(a background thread pool) to avoid blocking the main thread.onLoading: Optional composable displayed while parsing is in progress (e.g. a progress indicator or skeleton screen).
Behavior
- When
contentor the underlying parser instance changes, parsing restarts andonLoadingis invoked (if provided). - On success, the parsed AST is passed to the node-based
MarkdownViewunder the hood. - On failure,
onErroris invoked with the thrown exception.
Example
MarkdownView(
content = largeMarkdown,
markdownRenderConfig = MarkdownRenderConfig.Builder().build(),
modifier = Modifier.fillMaxSize(),
showNotSupportedText = true,
parseDispatcher = Dispatchers.IO, // or null to use MarkdownThreadPool.dispatcher
onLoading = {
CircularProgressIndicator()
},
onError = { error ->
Text(
text = "Failed to load markdown: ${error.message}",
color = MaterialTheme.colorScheme.error,
)
},
)Node-based version. Renders a pre-parsed Flexmark AST node.
Use this when you:
- Parse Markdown elsewhere (e.g. caching layer, background worker) and want to reuse the AST.
- Need to render the same content multiple times without re-parsing.
Signature (from MarkdownView.kt):
@Composable
fun MarkdownView(
node: Node,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
)Notes
- The
Nodeinstance is usually produced bymarkdownRenderConfig.parser.parse(content). - This overload does not know about loading or error states; you manage them yourself around the call.
Example
val config = remember { MarkdownRenderConfig.Builder().build() }
val parser = remember(config) { config.parser }
val node = remember(markdownText) { parser.parse(markdownText) }
MarkdownView(
node = node,
markdownRenderConfig = config,
)Lazy loading composable for very large markdown files stored on disk.
Instead of parsing and rendering the entire document at once, it:
- Splits content into chunks.
- Parses and renders chunks on demand as the user scrolls.
- Uses
LazyColumnwith prefetching to balance memory usage and performance.
Signature (from LazyMarkdownView.kt):
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyMarkdownView(
file: File,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
chunkLoaderConfig: ChunkLoaderConfig = ChunkLoaderConfig(
parserDispatcher = MarkdownThreadPool.dispatcher,
),
nestedPrefetchItemCount: Int = 3,
)Parameters
file: Local markdown file to display. For assets or network content, copy/download to a localFilefirst.markdownRenderConfig: Same config as used byMarkdownView.modifier: Applied to the internalLazyColumn.showNotSupportedText: Whether to show text for unsupported elements.actionHandler: Optional handler for actions inside the markdown content.chunkLoaderConfig: Controls how the file is split and parsed. There is a defaultparserDispatcherusingMarkdownThreadPool.dispatcher.nestedPrefetchItemCount: How many items before/after the visible viewport should be prefetched to make scrolling smoother.
Usage notes
- Best suited for read-only long-form content like books, large docs, etc.
- For editable or frequently changing content, prefer the async
MarkdownViewwith your own pagination or diffing.
Displays markdown content using LazyColumn for efficient rendering. Unlike LazyMarkdownView which lazily loads and parses content from a file in chunks, LazyMarkdownColumn parses the entire content upfront and only uses LazyColumn for display — each top-level block becomes a separate lazy item.
Signature (from LazyMarkdownColumn.kt):
@Composable
fun LazyMarkdownColumn(
content: String,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
lazyListState: LazyListState = rememberLazyListState(),
)Parameters
content: Markdown text to render.markdownRenderConfig: Same config as used byMarkdownView.modifier: Applied to the internalLazyColumn.showNotSupportedText: Whether to show text for unsupported elements.actionHandler: Optional handler for actions inside the markdown content.lazyListState: TheLazyListStatefor external scroll control. Defaults torememberLazyListState().
Usage notes
- Best for medium-to-large markdown content where you want
LazyColumnscrolling behavior instead ofverticalScroll. - Supports external scroll control via the
lazyListStateparameter.
Example
val config = remember { MarkdownRenderConfig.Builder().build() }
LazyMarkdownColumn(
content = markdownText,
markdownRenderConfig = config,
modifier = Modifier.fillMaxSize(),
)A utility composable that iterates over a parent node's children and renders them. It automatically handles Spacer insertion based on the current theme or provided parameters.
Signature (from MarkdownContent.kt):
@Composable
fun MarkdownChildren(
parent: Node,
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
spacerHeight: Dp = currentTheme().spacerTheme.spacerHeight,
showSpacer: Boolean = currentTheme().spacerTheme.showSpacer,
childModifierFactory: (child: Node) -> Modifier = {
Modifier
.wrapContentHeight()
.fillMaxWidth()
},
onBeforeChild: (@Composable (child: Node, parent: Node) -> Unit)? = null,
onAfterChild: (@Composable (child: Node, parent: Node) -> Unit)? = null,
onBeforeAll: (@Composable (parent: Node) -> Unit)? = null,
onAfterAll: (@Composable (parent: Node) -> Unit)? = null,
)Parameters
parent: TheNodewhose children you want to render.verticalArrangement: The vertical arrangement of the children.spacerHeight: Vertical spacing between children. Defaults totheme.spacerTheme.spacerHeight.showSpacer: Whether to insert spacers. Defaults totheme.spacerTheme.showSpacer.onBeforeChild: Optional Composable callback invoked before each child is rendered (useful for custom markers or indentation).onAfterChild: Optional Composable callback invoked after each child is rendered.onBeforeAll: Optional Composable callback invoked before rendering any children (e.g., for top padding/spacers).onAfterAll: Optional Composable callback invoked after rendering all children (e.g., for bottom padding/spacers).childModifierFactory: A factory function to create a Modifier for each child node.
Use Case
Use this when implementing a custom IBlockRenderer (e.g., for a custom container block) where you need to render nested content with standard spacing rules.
Note that MarkdownChildren provides a container layout (like Column); you can customize it with modifier.
Text-based rendering that renders the entire Markdown document through a single RichText composable, enabling cross-paragraph text selection. Parses the Markdown string synchronously inside a remember block.
Signature (from MarkdownText.kt):
@Composable
fun MarkdownText(
content: String,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
textAlign: TextAlign? = null,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
onTextLayout: (TextLayoutResult) -> Unit = {},
onError: (@Composable (Throwable) -> Unit)? = null,
)Parameters
content: The Markdown string to parse and render.markdownRenderConfig: The configuration for parsing and rendering.modifier: Modifier applied to the root layout.showNotSupportedText: Whether to show text for unsupported elements.actionHandler: Optional handler for link clicks and other actions.overflow: How visual overflow is handled (TextOverflow.Clip,Ellipsis, etc.).softWrap: Whether to break text at soft line breaks.textAlign: Text alignment.maxLines/minLines: Line count constraints for the rendered text.letterSpacing: Spacing between characters.textDecoration: Text decorations (underline, strikethrough).onTextLayout: Callback invoked withTextLayoutResultafter text layout.onError: Optional composable to display when parsing fails.
Example
SelectionContainer {
MarkdownText(
content = markdownContent,
markdownRenderConfig = config,
modifier = Modifier.padding(16.dp),
maxLines = 10,
overflow = TextOverflow.Ellipsis,
)
}Asynchronous version that parses on a background dispatcher. Distinguished from the sync version by the parseDispatcher and onLoading parameters.
Signature (from MarkdownText.kt):
@Composable
fun MarkdownText(
content: String,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
textAlign: TextAlign? = null,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
onTextLayout: (TextLayoutResult) -> Unit = {},
parseDispatcher: CoroutineDispatcher? = null,
onLoading: (@Composable () -> Unit)? = null,
onError: (@Composable (Throwable) -> Unit)? = null,
)Additional Parameters
parseDispatcher: TheCoroutineDispatcherto parse on. Defaults toMarkdownThreadPool.dispatcher.onLoading: Optional composable shown while parsing is in progress.
Pre-parsed version that accepts a flexmark Node directly.
Signature (from MarkdownText.kt):
@Composable
fun MarkdownText(
node: Node,
markdownRenderConfig: MarkdownRenderConfig,
modifier: Modifier = Modifier,
showNotSupportedText: Boolean = false,
actionHandler: ActionHandler? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
textAlign: TextAlign? = null,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
onTextLayout: (TextLayoutResult) -> Unit = {},
)Notes
- The
Nodeinstance is usually produced bymarkdownRenderConfig.parser.parse(content). - This overload does not handle loading or error states; you manage them yourself.
Holds everything needed to parse and render markdown:
- A
MarkdownThemedescribing typography, colors and component styles. - A Flexmark
ParserandHtmlRenderer. - A
RenderRegistrydescribing how nodes and blocks are rendered.
Instances are created via MarkdownRenderConfig.Builder.
val config = MarkdownRenderConfig.Builder()
// configure theme, plugins, renderers...
.build()You typically create one MarkdownRenderConfig per screen or share it via remember / DI.
Fluent builder to customize parsing, theming and rendering behavior.
Key methods (from MarkdownRenderConfig.kt):
class MarkdownRenderConfig {
class Builder {
fun markdownTheme(markdownTheme: MarkdownTheme): Builder
fun addPlugin(plugin: IMarkdownRenderPlugin): Builder
fun addInlineNodeStringBuilder(
nodeClass: Class<out Node>,
builder: IInlineNodeStringBuilder<*>,
): Builder
fun addBlockRenderer(
blockClass: Class<out Block>,
renderer: IBlockRenderer<*>,
): Builder
fun markdownTextRenderer(renderer: MarkdownTextRenderer): Builder
fun markdownContentRenderer(renderer: MarkdownContentRenderer): Builder
fun addBlockParserFactory(factory: CustomBlockParserFactory): Builder
fun addInlineContentParserFactory(factory: InlineParserExtensionFactory): Builder
fun addDelimiterProcessor(processor: DelimiterProcessor): Builder
fun addExtension(extension: Extension): Builder
fun build(): MarkdownRenderConfig
}
}Sets the visual theme for markdown rendering.
- If not set, the library uses
MarkdownTheme()with default styles. - Themes typically encapsulate typography, colors, spacings and decorations for:
- Paragraphs, headings, emphasis, strong emphasis
- Code blocks, inline code
- Lists, block quotes, tables, and code blocks
Registers a render plugin.
Plugins can:
- Contribute Flexmark extensions.
- Register extra block/inline renderers.
- Add custom block parsers or inline parser extensions.
Built-in core functionality is provided by an internal CorePlugin which is always added automatically.
Low-level hooks to customize rendering of specific nodes/blocks.
addInlineNodeStringBuilder(nodeClass, builder): defines how a given inline node type is converted to text spans.addBlockRenderer(blockClass, renderer): defines how a given block node type is rendered as Compose UI.
These are typically used inside your own IMarkdownRenderPlugin implementation, but can also be used directly on the builder if you just need a small adjustment.
Advanced customization points:
markdownTextRenderer: Override how text nodes are rendered (e.g., custom text shaping or layout).markdownContentRenderer: Override rendering of higher-level content nodes.
Most users won't need these; theme and plugins are usually enough.
Flexmark-level extension APIs:
addBlockParserFactory: Register custom block parsers (for new block syntaxes).addInlineContentParserFactory: Register inline parser extensions.addDelimiterProcessor: Handle custom delimiters (e.g., for new emphasis markers).addExtension: Add raw FlexmarkExtensioninstances.
These are mainly intended for advanced/custom markdown dialects.
MarkdownTheme (from com.iffly.compose.markdown.style.MarkdownTheme) is the central theme model
for how markdown content is rendered in Compose.
It controls:
- Base text style (font size, line height, family, color)
- Emphasis / strong emphasis / strikethrough / subscript styles
- Link styles (normal / hovered / focused / pressed)
- Heading styles (H1–H6)
- Layout & look of lists, images, block quotes, spacers, tables, and code blocks
@Stable
data class MarkdownTheme(
val breakLineHeight: Dp = 1.dp,
val breakLineColor: Color = Color(0xFFE0E0E0),
val textStyle: TextStyle = TextStyle(...),
val strongEmphasis: SpanStyle = SpanStyle(...),
val emphasis: SpanStyle = SpanStyle(...),
val code: TextStyle = TextStyle(...),
val strikethrough: SpanStyle = SpanStyle(...),
val subscript: SpanStyle = SpanStyle(...),
val link: TextLinkStyles = TextLinkStyles(...),
val headStyle: Map<Int, TextStyle> = defaultHeadStyles,
val listTheme: ListTheme = ListTheme(),
val blockQuoteTheme: BlockQuoteTheme = BlockQuoteTheme(),
val spacerTheme: SpacerTheme = SpacerTheme(),
val codeBlockTheme: CodeBlockTheme = CodeBlockTheme(),
)Key nested types (from MarkdownTheme.kt):
BlockQuoteTheme– border color/width, background, shape, paddingSpacerTheme– whether to show spacers between blocks, heightListTheme– bullet/number marker spacing and styleCodeBlockTheme– code block text style, background, padding, overflow, etc.
Note:
TableThemeis now part ofTableMarkdownPluginconfiguration, notMarkdownTheme.ImageThemeis now part ofImageMarkdownPluginconfiguration, notMarkdownTheme.
Headings are configured via the headStyle map. Keys are integer levels 1–6 and are exposed as
constants on MarkdownTheme:
MarkdownTheme.HEAD1 // level 1 (H1)
MarkdownTheme.HEAD2 // level 2 (H2)
// ... up to HEAD6Example override:
val theme = MarkdownTheme(
headStyle = mapOf(
MarkdownTheme.HEAD1 to
TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
),
MarkdownTheme.HEAD2 to
TextStyle(
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
),
),
)link uses TextLinkStyles from Compose to describe the visual states of links:
val theme = MarkdownTheme(
link =
TextLinkStyles(
style =
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
),
hoveredStyle =
SpanStyle(
color =
MaterialTheme.colorScheme.primary.copy(
alpha = 0.8f,
),
textDecoration = TextDecoration.Underline,
),
focusedStyle =
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
),
pressedStyle =
SpanStyle(
color =
MaterialTheme.colorScheme.primary.copy(
alpha = 0.6f,
),
textDecoration = TextDecoration.Underline,
),
),
)ListTheme controls spacing and marker text style:
@Immutable
data class ListTheme(
val markerSpacerWidth: Dp = 4.dp,
val showSpacerInTightList: Boolean = true,
val tightListSpacerHeight: Dp = 4.dp,
val markerTextStyle: TextStyle? =
TextStyle(
lineHeight = 24.sp,
fontSize = 17.sp,
textAlign = TextAlign.End,
),
)Typical customisation:
- Increase
markerSpacerWidthwhen you use wide bullets/checkboxes - Adjust
markerTextStyleto better align with the body text
Moved to ImageMarkdownPlugin. See the Plugins section for details on how to configure ImageTheme.
BlockQuoteTheme decides the look of > quoted blocks:
@Immutable
data class BlockQuoteTheme(
val borderColor: Color = Color.LightGray,
val borderWidth: Dp = 5.dp,
val backgroundColor: Color = Color(0xFFF5F5F5),
val shape: Shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp),
val padding: PaddingValues = PaddingValues(horizontal = 12.dp),
)You can adjust it to match Material3 surface styles or your brand color.
TableTheme and CodeBlockTheme cover more advanced layouts such as tables and fenced code blocks.
They expose:
- Border color, thickness, row background colors
- Cell/header text styles, padding, shape
- Code text style, background, padding, overflow behaviours, etc.
When you need a full reference of fields, open MarkdownTheme.kt in your IDE. The main idea is that
you do all visual tuning through MarkdownTheme, then provide it to
MarkdownRenderConfig.Builder().markdownTheme(theme).
Putting it together:
val markdownTheme =
MarkdownTheme(
textStyle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp),
strongEmphasis =
SpanStyle(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
),
emphasis =
SpanStyle(
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
),
headStyle =
mapOf(
MarkdownTheme.HEAD1 to
TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
),
),
)
val config = MarkdownRenderConfig.Builder()
.markdownTheme(markdownTheme)
.build()
MarkdownView(
content = markdownContent,
markdownRenderConfig = config,
)ChunkLoaderConfig (from com.iffly.compose.markdown.chunkloader) controls how LazyMarkdownView splits and parses large files.
Typical fields include:
parserDispatcher: Dispatcher used for background parsing (defaults toMarkdownThreadPool.dispatcher).- Other values controlling:
- Initial number of lines/chunks to load.
- Incremental load size when the user scrolls.
- Maximum number of chunks kept in memory.
The exact fields may evolve; refer to the ChunkLoaderConfig source for the latest list.
Example (conceptual):
val chunkConfig = ChunkLoaderConfig(
// initialLines = 800,
// incrementalLines = 400,
// maxCachedChunks = 8,
parserDispatcher = MarkdownThreadPool.dispatcher,
)
LazyMarkdownView(
file = largeFile,
markdownRenderConfig = config,
chunkLoaderConfig = chunkConfig,
)Entry point for adding features to the markdown engine.
A plugin can:
- Provide Flexmark extensions.
- Register inline string builders and block renderers.
- Contribute block parser factories and inline parser extension factories.
The exact interface is defined in IMarkdownRenderPlugin (see the config or core packages).
Plugins are added via MarkdownRenderConfig.Builder.addPlugin.
The library is built on Flexmark. You can extend parsing and rendering of Block (block-level) and Inline nodes by implementing IMarkdownRenderPlugin. In practice, you usually only need to know a few main entry points:
-
Custom Block parsing:
- Use Flexmark
CustomBlockParserFactoryto define new block syntaxes (for example an:::alertstyle block). - Return your factories from
IMarkdownRenderPlugin.blockParserFactories()so the parser can recognize these blocks. - Implement
IBlockRenderer<YourBlock>for your custom Block type and register it inblockRenderers()to provide corresponding Compose UI rendering.
- Use Flexmark
-
Custom Inline parsing:
- Implement
InlineParserExtensionFactoryto define new inline syntaxes (for example@user,#tag, or==highlight==). - Return your factories from
IMarkdownRenderPlugin.inlineContentParserFactories()so the parser can find these markers inside text. - If you need to control how these inline nodes are turned into text or rich inline content, register an
IInlineNodeStringBuilderfor the corresponding FlexmarkNodetype viainlineNodeStringBuilders().
- Implement
The typical flow is: use Flexmark extension points to define syntax → register parsing and rendering strategies in IMarkdownRenderPlugin → enable the plugin via MarkdownRenderConfig.Builder.addPlugin(...). You can refer to the CustomMarkdownPlugin sample for a concrete end-to-end example.
For more details about Flexmark extension points and parser customization, see the official docs: flexmark-java
Responsible for rendering a specific Flexmark Block node type as Compose UI.
- Registered via
addBlockRendereron the builder or through a plugin. - Allows you to provide completely custom visuals for certain block elements.
Interface signature (simplified) (see IBlockRenderer.kt):
interface IBlockRenderer<in T> where T : Block {
@Composable
fun Invoke(
node: T,
modifier: Modifier,
)
}Parameters
T: The block node type to render. Must be a FlexmarkBlocksubclass such asParagraph,Heading,BlockQuote,TableBlock, etc.node: The concrete block node instance. Typically you will:- Iterate its children and delegate to
MarkdownContent/MarkdownTextor other existing composables; - Or read block‑specific fields (for example the language info on
FencedCodeBlock.info).
- Iterate its children and delegate to
modifier: TheModifierpassed from the outer markdown pipeline. Implementations should apply this modifier first and then.then(...)any custom modifiers, rather than discarding it, so that layout and outer containers remain consistent.
Implementation notes
- Prefer using
MarkdownThemeto configure styling (colors, typography, paddings) and let custom block renderers focus on structure and layout (rows, columns, cards, backgrounds, spacing, etc.). - If your renderer needs to trigger actions (copy, open link, expand, etc.), obtain the current
ActionHandlerviacurrentActionHandler()instead of storing your own reference, so you stay aligned with the library's lifecycle. - Once registered, a custom
IBlockRendererfully replaces the default renderer for thatBlocktype. Make sure you handle all relevant child nodes or explicitly delegate back to the default behaviour to avoid missing content.
Responsible for converting a specific inline Node into styled text spans.
- Registered via
addInlineNodeStringBuilder. - Used internally by the text renderer when building annotated strings.
Interface signature (simplified) (see IInlineNodeStringBuilder.kt):
interface IInlineNodeStringBuilder<in T> where T : Node {
fun AnnotatedString.Builder.buildInlineNodeString(
node: T,
inlineContentMap: MutableMap<String, MarkdownInlineView>,
markdownTheme: MarkdownTheme,
actionHandler: ActionHandler?,
indentLevel: Int,
isShowNotSupported: Boolean,
renderRegistry: RenderRegistry,
nodeStringBuilderContext: NodeStringBuilderContext,
)
}Parameters
T: The inline node type to handle, e.g.Text,Emphasis,StrongEmphasis,Link, etc.node: The inline node instance. You read its contents (text, URL, emphasis level, etc.) to decide what spans or inline content to build.inlineContentMap: A mutable map used to register rich inline content:- Keys are unique strings; values are
MarkdownInlineViewinstances. - Builders typically insert
MarkdownRichTextInlineContenthere and then reference it from theAnnotatedStringviaappendInlineContent(key, placeholder). - Be aware: keys behave like stable Compose keys — if you change only the content and keep the same key, existing inline content will not be recreated.
- Keys are unique strings; values are
markdownTheme: The activeMarkdownTheme. Prefer pullingSpanStyle/ParagraphStylefrom here instead of hard‑coding colours, font sizes, etc., so that your builder stays theme‑aware.actionHandler: Optional handler for interactions:- In the built‑in
Linkbuilder it is used to createMarkdownLinkInteractionListener. - For custom inline components (mentions, tags, etc.) you can use it to forward click events to your app logic.
- In the built‑in
indentLevel: Current indentation level (mainly relevant for nested lists):- Can be used to compute leading indentation or marker spacing;
- Often combined with
ListTheme.markerSpacerWidthwhen rendering list bullets and numbers.
isShowNotSupported: Controls whether to emit a visible placeholder for unsupported nodes:- When
true, you may output a fallback like"[unsupported: NodeName]"instead of silently skipping the node; - When
false, you can safely ignore unsupported nodes.
- When
renderRegistry: The render registry for the current configuration:- Used by helper utilities such as
buildChildNodeAnnotatedStringto recursively render child nodes; - Lets your builder compose with other registered inline builders instead of re‑implementing traversal.
- Used by helper utilities such as
nodeStringBuilderContext: Node string build context:- Provides grouped subcontexts for layout (
layoutContext), text style (designContext), and platform/system access (systemContext); - Use
nodeStringBuilderContext.layoutContextwhen text measurement or density conversion is needed.
- Provides grouped subcontexts for layout (
Implementation notes
- When adding styling, prefer using
pushStyle/pop,withLink, and similarAnnotatedString.Builderhelpers instead of rebuilding the whole string from scratch. - For nodes that can contain children (e.g.
Link,Emphasis), delegate to the common "build children" helper that usesrenderRegistryinstead of manually handling every possible child type; this way new node types added later are handled automatically. - Avoid storing
Contextor Compose state inside builders. They should be pure string‑building utilities; side‑effects and state belong in composables or theActionHandlerlayer.
MarkdownInlineView is a small sealed interface used to represent inline composable content
that can be embedded inside text. It is primarily consumed by inline node string builders when they
need to register rich inline content instead of plain styled text.
Defined in render/MarkdownInlineView.kt:
sealed interface MarkdownInlineView {
data class MarkdownRichTextInlineContent(
val inlineContent: RichTextInlineContent,
) : MarkdownInlineView
}Key points:
MarkdownInlineViewis the value type used ininlineContentMap: MutableMap<String, MarkdownInlineView>parameters ofIInlineNodeStringBuilderimplementations.MarkdownRichTextInlineContentwraps aRichTextInlineContentinstance, which knows how to render a composable inline element (icon, chip, badge, custom widget, etc.).- Inline builders register entries in the
inlineContentMapwith a unique key; the text renderer then references those keys from theAnnotatedStringto display rich inline content alongside normal text. - notice: If you only change the content and do not change the key in
inlineContentMap, the inline content will not update.
Conceptual usage in a custom inline builder
class IconInlineNodeStringBuilder : IInlineNodeStringBuilder<IconNode> {
override fun AnnotatedString.Builder.buildInlineNodeString(
node: IconNode,
inlineContentMap: MutableMap<String, MarkdownInlineView>,
markdownTheme: MarkdownTheme,
actionHandler: ActionHandler?,
indentLevel: Int,
isShowNotSupported: Boolean,
renderRegistry: RenderRegistry,
nodeStringBuilderContext: NodeStringBuilderContext,
) {
// 1. Register rich inline content under a unique key
val key = "icon-${node.id}"
inlineContentMap[key] =
MarkdownInlineView.MarkdownRichTextInlineContent(
inlineContent = /* build RichTextInlineContent for this icon */
buildIconInlineContent(node),
)
// 2. Append a placeholder character with that key as an annotation
appendInlineContent(key, " ")
}
}You typically do not manipulate MarkdownInlineView directly outside of custom
IInlineNodeStringBuilder implementations. It forms part of the bridge between flexmark inline
nodes and Compose rich text inline content.
MarkdownRichTextInlineContent internally wraps a RichTextInlineContent (defined in
widget/richtext/RichTextInlineContent.kt). There are currently two variants:
-
EmbeddedRichTextInlineContent- Characteristics
- Uses a
Placeholderto define width, height and alignment, and participates in the same paragraph layout as the surrounding text. - The optional
adjustSizeByContentflag allows fine-tuning the size based on the actual content. content: @Composable (String) -> Unitreceives a key or placeholder text and renders a small inline composable (icon, badge, etc.).
- Uses a
- Recommended scenarios
- Small inline icons (e.g. "⚠", "✔"), emoji, status dots, count badges and other components that should appear on the same line as text.
- Custom views that must align precisely with the text baseline and line height.
- For example, showing an info icon before
**[info] text**, or a verification badge after a username.
- Characteristics
-
StandaloneInlineContent- Characteristics
- Uses a
modifier: Modifier = Modifierto control the overall layout and size. content: @Composable (modifier: Modifier) -> Unitrenders a fully custom Compose component.- In the text layout it is treated as a separate paragraph/block, not mixed inline with text.
- Uses a
- Recommended scenarios
- Inserting whole custom blocks into Markdown, such as ad cards, button areas, statistics cards, rich media modules, etc.
- Content that should have vertical spacing and be laid out independently from normal paragraphs.
- For example, inserting a recommendation card, banner or voting widget between two Markdown paragraphs.
- Characteristics
In a custom IInlineNodeStringBuilder:
- Use
EmbeddedRichTextInlineContentwhen the component should appear inline with text and share the same line. - Use
StandaloneInlineContentwhen the component should occupy its own paragraph/block in the layout.
At the heart of the rendering pipeline is RenderRegistry, which decides how each flexmark node
is turned into Compose UI.
RenderRegistry is constructed inside MarkdownRenderConfig.Builder.build() and receives:
blockRenderers: Map<Class<out Block>, IBlockRenderer<*>>inlineNodeStringBuilders: Map<Class<out Node>, IInlineNodeStringBuilder<*>>- Optional
MarkdownContentRenderer - Optional
MarkdownTextRenderer
You usually interact with it indirectly via:
Builder.addBlockRenderer(...)Builder.addInlineNodeStringBuilder(...)- Plugins that implement
IMarkdownRenderPluginand contribute their own renderers.
The library ships with a set of built‑in block renderers and inline node string builders that implement the default CommonMark/GFM behaviour. Understanding them helps when you want to override a specific part of the rendering pipeline.
File: core/renders/TextBlockRenderer.kt
open class TextBlockRenderer<T> : IBlockRenderer<T> where T : Block {
@Composable
override fun Invoke(node: T, modifier: Modifier) {
MarkdownText(parent = node, modifier = modifier)
}
}
class ParagraphRenderer : TextBlockRenderer<Paragraph>()
class HeadingRenderer : TextBlockRenderer<Heading>()TextBlockRendereris a generic block renderer that delegates toMarkdownTextto render all inline children of a block (paragraphs, headings, etc.).ParagraphRendererandHeadingRendererare concrete implementations used forParagraphandHeadingnodes.
If you only want to tweak text styling, prefer changing MarkdownTheme instead of overriding
these renderers. Override them only when you need completely different layouts for paragraphs or
headings.
File: core/renders/BlockQuoteRenderer.kt
class BlockQuoteRenderer : IBlockRenderer<BlockQuote> {
@Composable
override fun Invoke(node: BlockQuote, modifier: Modifier) {
MarkdownBlockQuote(node = node, modifier = modifier)
}
}MarkdownBlockQuote uses the current MarkdownTheme to style quotes:
theme.blockQuoteTheme.borderColor / borderWidthbackgroundColor,shape,paddingtheme.spacerTheme.spacerHeightfor top/bottom spacing
It then iterates over children and calls MarkdownContent to render them. To customize block quotes
visually, adjust MarkdownTheme.blockQuoteTheme; to radically change their layout, you can provide
your own IBlockRenderer<BlockQuote> and register it via addBlockRenderer.
File: core/renders/ParagraphNodeStringBuilder.kt
class ParagraphNodeStringBuilder : CompositeChildNodeStringBuilder<Node>() {
override fun getParagraphStyle(
node: Node,
markdownTheme: MarkdownTheme,
): ParagraphStyle? = markdownTheme.getNodeParagraphStyle(node)
}This is a default inline string builder for paragraph‑like nodes. It:
- Delegates to
markdownTheme.getNodeParagraphStyle(node)(a helper in the util package) to pick aParagraphStylebased on the node type and current theme. - Uses
CompositeChildNodeStringBuilderto iterate over child nodes and compose their text.
If you want to change paragraph alignment, spacing, or other paragraph‑level behaviour globally,
you can adjust how getNodeParagraphStyle reads from MarkdownTheme (or provide your own
CompositeChildNodeStringBuilder implementation).
File: core/renders/HeadingNodeStringBuilder.kt
class HeadingNodeStringBuilder : CompositeChildNodeStringBuilder<Heading>() {
override fun getSpanStyle(
node: Heading,
markdownTheme: MarkdownTheme,
): SpanStyle? = markdownTheme.getNodeSpanStyle(node)
override fun getParagraphStyle(
node: Heading,
markdownTheme: MarkdownTheme,
): ParagraphStyle? = markdownTheme.getNodeParagraphStyle(node)
}For headings, this builder:
- Uses
markdownTheme.getNodeSpanStyle(node)to choose aSpanStyle(typically mapped fromMarkdownTheme.headStylefor H1–H6). - Uses
markdownTheme.getNodeParagraphStyle(node)for paragraph‑level styling.
This is the link between MarkdownTheme.headStyle and an actual rendered heading. To customise
heading typography, colors or spacing, configure MarkdownTheme instead of replacing this
string builder.
File: core/renders/LinkNodeStringBuilder.kt
class LinkNodeStringBuilder : IInlineNodeStringBuilder<Link> {
override fun AnnotatedString.Builder.buildInlineNodeString(
node: Link,
inlineContentMap: MutableMap<String, MarkdownInlineView>,
markdownTheme: MarkdownTheme,
actionHandler: ActionHandler?,
indentLevel: Int,
isShowNotSupported: Boolean,
renderRegistry: RenderRegistry,
nodeStringBuilderContext: NodeStringBuilderContext,
) {
val linkInteractionListener =
actionHandler?.let {
MarkdownLinkInteractionListener(actionHandler = it, node = node)
}
val linkAnnotation =
LinkAnnotation.Url(
url = node.url.toString(),
styles = markdownTheme.link,
linkInteractionListener = linkInteractionListener,
)
withLink(linkAnnotation) {
buildChildNodeAnnotatedString(
node,
indentLevel,
inlineContentMap,
markdownTheme,
renderRegistry,
actionHandler,
isShowNotSupported,
nodeStringBuilderContext,
)
}
}
}Key points:
- It wraps the link’s children inside a
LinkAnnotation.Urlwith styles frommarkdownTheme.link. - If an
ActionHandleris provided, it creates aMarkdownLinkInteractionListenerso that link clicks can be forwarded to your app code. - Actual inline contents are produced via
buildChildNodeAnnotatedString, which uses other inline builders registered inRenderRegistry.
To change link behaviour:
- For visuals, adjust
MarkdownTheme.link. - For interaction, implement your own
ActionHandlerto handle link clicks. - For very custom links, you can provide your own
IInlineNodeStringBuilder<Link>and register it withaddInlineNodeStringBuilder.
TableMarkdownPlugin (in markdown-table module)
File: markdown-table/src/main/java/com/iffly/compose/markdown/table/MarkdownTable.kt
This module provides a fully featured table renderer based on flexmark's TableBlock extension and
a small Compose Table widget DSL.
Key types:
TableWidgetRenderer<T : Node>– fun interface for rendering parts of a table.TableTitleRenderer– default renderer for a table title/header area.TableCellRenderer– default renderer for each table cell.TableRenderer–IBlockRenderer<TableBlock>that wires everything together.MarkdownTable– top-level composable that lays out the table.
fun interface TableWidgetRenderer<T : Node> {
@Composable
operator fun invoke(node: T, modifier: Modifier)
}
class TableTitleRenderer : TableWidgetRenderer<TableBlock> { /* ... */ }
class TableCellRenderer : TableWidgetRenderer<TableCell> { /* ... */ }
class TableRenderer(
private val tableTitleRenderer: TableWidgetRenderer<TableBlock> = TableTitleRenderer(),
private val tableCellRenderer: TableWidgetRenderer<TableCell> = TableCellRenderer(),
) : IBlockRenderer<TableBlock> {
@Composable
override fun Invoke(node: TableBlock, modifier: Modifier) {
MarkdownTable(
tableBlock = node,
modifier = modifier,
tableTitleRenderer = tableTitleRenderer,
tableCellRenderer = tableCellRenderer,
)
}
}MarkdownTable uses MarkdownTheme.tableTheme for:
borderColor,borderThicknesstitleBackgroundColor,tableHeaderBackgroundColor,tableCellBackgroundColorcellPadding,cellTextStyle,headerTextStyle,copyTextStyle,shape
and builds a horizontally scrollable table when there are many columns.
Customising table title only
You can override just the title widget while reusing default cell rendering:
class CustomTableTitleRenderer : TableWidgetRenderer<TableBlock> {
@Composable
override fun invoke(node: TableBlock, modifier: Modifier) {
// Render custom title UI, e.g. title + row/column count + actions
Row(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("Table", style = MaterialTheme.typography.titleSmall)
Text("${node.cells().size} rows")
}
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
TableBlock::class.java,
TableRenderer(
tableTitleRenderer = CustomTableTitleRenderer(),
),
)
.build()Customising cell widget
To change only the way cells are rendered (e.g. center all numeric values, add icons, etc.):
class CustomTableCellRenderer : TableWidgetRenderer<TableCell> {
@Composable
override fun invoke(node: TableCell, modifier: Modifier) {
val theme = currentTheme()
val isHeader = node.parent is TableRow && node.parent?.parent is TableHead
SelectionContainer {
MarkdownText(
parent = node,
modifier = Modifier,
textAlign = TextAlign.Center, // force center for all cells
textStyle = if (isHeader) theme.tableTheme.headerTextStyle else theme.tableTheme.cellTextStyle,
)
}
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
TableBlock::class.java,
TableRenderer(tableCellRenderer = CustomTableCellRenderer()),
)
.build()You can combine both tableTitleRenderer and tableCellRenderer overrides if you need full control
but still want to reuse MarkdownTable’s layout and scrolling behaviour.
File: core/renders/MarkdownCodeBlock.kt
This module contains composable widgets for fenced and indented code blocks:
CodeAnnotator– fun interface for styling raw code text as anAnnotatedString(e.g. syntax highlighting).CodeWidgetRenderer<T : Block>– fun interface for small code block widgets.CopyRenderer<T>– default "Copy" button.CodeHeaderRenderer<T>– default header with language label and copy button.CodeContentRenderer<T>– default code content with line numbers and optional scroll.CodeRenderer<T : Block>– base implementation combining header + content.FencedCodeBlockRenderer/IndentedCodeBlockRenderer– concreteIBlockRendererimplementations.
fun interface CodeAnnotator {
fun annotate(code: String, language: String, node: Block): AnnotatedString
}
fun interface CodeWidgetRenderer<T : Block> {
@Composable
operator fun invoke(block: T, modifier: Modifier)
}
class FencedCodeBlockRenderer(
renderCopyOverride: CodeWidgetRenderer<FencedCodeBlock>? = null,
renderContentOverride: CodeWidgetRenderer<FencedCodeBlock>? = null,
renderHeaderOverride: CodeWidgetRenderer<FencedCodeBlock>? = null,
codeAnnotator: CodeAnnotator? = null,
) : IBlockRenderer<FencedCodeBlock> by CodeRenderer(
renderCopyOverride,
renderContentOverride,
renderHeaderOverride,
codeAnnotator,
)CodeRenderer uses MarkdownTheme.codeBlockTheme for:
backgroundColor,borderWidth,borderColor,shapeblockModifier,headerModifiershowHeader,showCopyButtoncodeTitleTextStyle,codeCopyTextStylecontentTheme(code font, line numbers, padding, height, softWrap, etc.)
Syntax highlighting with BasicSyntaxHighlighter
The library ships a ready-made CodeAnnotator that applies regex-based token coloring for 20+
languages. Pass a BasicSyntaxHighlighter instance as codeAnnotator to enable it:
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
FencedCodeBlock::class.java,
FencedCodeBlockRenderer(
codeAnnotator = BasicSyntaxHighlighter(),
),
)
.addBlockRenderer(
IndentedCodeBlock::class.java,
IndentedCodeBlockRenderer(
codeAnnotator = BasicSyntaxHighlighter(),
),
)
.build()Customize the token colors via CodeColors:
val highlighter = BasicSyntaxHighlighter(
colors = CodeColors(
keyword = Color(0xFF569CD6),
string = Color(0xFFCE9178),
comment = Color(0xFF6A9955),
number = Color(0xFFB5CEA8),
annotation = Color(0xFFDCDC00),
type = Color(0xFF4EC9B0),
)
)Supported languages (fenced code fence info string, case-insensitive):
kotlin, java, javascript/js, typescript/ts, python, swift, rust, go, dart,
c, cpp/c++, cs/csharp/c#, ruby, php, sql, bash/sh/shell/zsh,
css, html, xml, yaml/yml, toml, json.
Implement CodeAnnotator directly to integrate any third-party syntax highlighter:
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
FencedCodeBlock::class.java,
FencedCodeBlockRenderer(
codeAnnotator = CodeAnnotator { code, language, _ ->
myHighlighter.highlight(code, language)
},
),
)
.build()Customising only the copy button
class IconCopyRenderer<T : Block> : CodeWidgetRenderer<T> {
@Composable
override fun invoke(block: T, modifier: Modifier) {
val actionHandler = currentActionHandler()
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy code",
modifier = modifier.clickable {
actionHandler?.handleCopyClick(block)
},
)
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
FencedCodeBlock::class.java,
FencedCodeBlockRenderer(
renderCopyOverride = IconCopyRenderer(),
),
)
.build()Customising the header
For example, show file name, language tag, or a custom toolbar:
class CustomCodeHeaderRenderer<T : Block> : CodeWidgetRenderer<T> {
@Composable
override fun invoke(block: T, modifier: Modifier) {
val theme = currentTheme().codeBlockTheme
val language = (block as? FencedCodeBlock)?.info?.toString().orEmpty()
Row(
modifier =
modifier
.fillMaxWidth()
.then(theme.headerModifier),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = language.ifBlank { "Code" }, style = theme.codeTitleTextStyle)
// You can add more actions here (run, copy, expand, etc.)
}
HorizontalDivider(
color = theme.borderColor,
thickness = theme.borderWidth,
)
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
FencedCodeBlock::class.java,
FencedCodeBlockRenderer(
renderHeaderOverride = CustomCodeHeaderRenderer(),
),
)
.build()Customising the content widget
If you want to fully control how code is displayed (e.g. show only part of the code with "show more"):
class CustomCodeContentRenderer<T : Block> : CodeWidgetRenderer<T> {
@Composable
override fun invoke(block: T, modifier: Modifier) {
val code = when (block) {
is FencedCodeBlock -> block.contentChars.toString()
is IndentedCodeBlock -> block.contentChars.toString()
else -> return
}
// Replace LineNumberText with your own composable
MySyntaxHighlightedCode(
code = code,
modifier = modifier
.verticalScroll(rememberScrollState()),
)
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(
FencedCodeBlock::class.java,
FencedCodeBlockRenderer(renderContentOverride = CustomCodeContentRenderer()),
)
.build()You can mix and match codeAnnotator, renderCopyOverride, renderHeaderOverride, and
renderContentOverride when constructing FencedCodeBlockRenderer / IndentedCodeBlockRenderer,
depending on which part of the widget you want to take over. Note that codeAnnotator is forwarded
to the default CodeContentRenderer and is ignored when renderContentOverride is provided.
ImageMarkdownPlugin (in markdown-image module)
You can customize the image appearance by passing an ImageTheme to the plugin constructor:
.addPlugin(
ImageMarkdownPlugin(
imageTheme = ImageTheme(
alignment = Alignment.Center,
contentScale = ContentScale.Inside,
// ...
)
)
)ImageTheme Configuration:
data class ImageTheme(
val alignment: Alignment = Alignment.Center,
val contentScale: ContentScale = ContentScale.Inside,
val shape: Shape = RoundedCornerShape(8.dp),
val modifier: Modifier = Modifier,
val errorPlaceholderColor: Color = Color(0xFFE0E0E0),
)File: markdown-image/src/main/java/com/iffly/compose/markdown/image/MarkdownImage.kt
MarkdownImage is the default image composable used for flexmark Image nodes. It is built on top
of Coil 3 and exposes hooks to customise loading and error UI.
@Composable
fun MarkdownImage(
node: Image,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Inside,
errorView: @Composable (image: Image, modifier: Modifier) -> Unit = { image, modifier ->
MarkdownImageErrorView(
modifier = modifier,
altText = image.text?.toString() ?: image.title?.toString(),
)
},
loadingView: @Composable (image: Image, modifier: Modifier) -> Unit = { image, modifier ->
LoadingView(modifier = modifier)
},
)Internally it uses:
SubcomposeAsyncImagewith anImageRequestbuilt fromnode.url.currentActionHandler()to forward image clicks viahandleImageClick(url, node).MarkdownImageErrorViewas the default error placeholder (ic_image_errordrawable).
Custom loading / error widgets
You can replace just the loading or error composables while keeping the rest of the behaviour:
@Composable
fun CustomLoading(image: Image, modifier: Modifier) {
Box(modifier, contentAlignment = Alignment.Center) {
CircularProgressIndicator(strokeWidth = 2.dp)
}
}
@Composable
fun CustomError(image: Image, modifier: Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(Icons.Default.BrokenImage, contentDescription = null)
Text(text = image.text?.toString().orEmpty())
}
}Using a custom image renderer
To apply these custom widgets to all markdown images, you can register a custom block renderer for
flexmark Image nodes that wraps MarkdownImage:
class CustomImageRenderer : IBlockRenderer<Image> {
@Composable
override fun Invoke(node: Image, modifier: Modifier) {
MarkdownImage(
node = node,
modifier = modifier,
loadingView = ::CustomLoading,
errorView = ::CustomError,
)
}
}
val config = MarkdownRenderConfig.Builder()
.addBlockRenderer(Image::class.java, CustomImageRenderer())
.build()This pattern mirrors how you customise tables and code blocks: you override only the widget you care about (loading/error/content), while leaving the rest of the markdown pipeline unchanged.
- Use synchronous
MarkdownView. - Default
MarkdownRenderConfig.Builder().build()is usually enough.
- Use asynchronous
MarkdownView. - Provide
onLoadingandonErrorcomposables. - Optionally set
parseDispatcher = Dispatchers.IO(or leavenullto useMarkdownThreadPool.dispatcher).
- Use
LazyMarkdownViewwith aFile. - Tune
ChunkLoaderConfigto balance load speed vs memory.
- Create a shared
MarkdownRenderConfig:- Set
markdownThemeto match your design system. - Add plugins for tasks, LaTeX, custom alerts, etc.
- Optionally register custom block renderers or inline builders.
- Set