Markdown with Markwon
In this post we'll explore using the Markwon library to display
markdown documents inside of an android app. The library comes with excellent documentation and it works its magic on textviews so we won't need any webviews!
We'll explore the library's major features by using one of our previous blog posts as the sample markdown. That way we'll be able to see how the library handles plain old markdown text, images, and code blocks. You can follow along here or checkout the post's accompanying project here
Let's begin by creating a simple project in android studio using the empty activity project template. Once we've got the project created we'll modify our app's build.gradle file to add everything we'll need to achieve our goal of displaying markdown with images and syntax highlighting in code blocks.
First we'll need to apply the kotlin kapt plugin at the top of our gradle file
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
Then we'll need to add a couple of markwon libraries.
implementation "io.noties.markwon:core:4.6.2"
implementation 'io.noties.markwon:syntax-highlight:4.6.2'
implementation 'io.noties.markwon:image:4.6.2'
kapt 'io.noties:prism4j-bundler:2.0.0'
The library names are pretty self explanatory as far as what they will bring to the table for us except for that last one. The prism4j bundler. That kapt dependency will allow us to autogenerate a grammar locator class that markwon will need in order to highlight code syntax. (Don't worry you'll see where and how the grammar locator is used in a moment)
The code the bundler will write for us will save us a lot of work, but everything it does is something you could do by hand if you chose to do so. In fact if your favorite language isn't supported by the bundler but you still wanted to use markwon to highlight syntax in that language you could write everything yourself.
The final snippet of code is a workaround for a dependency conflict you'll have after adding these dependencies to your project. Specifically the conflict comes from the prism4j dependency relying on a very old version of annotations. We just tell gradle to exclude that and count on everything being ok with the more modern annotation dependency brought in by the kotlin std lib.
configurations.all {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
Alright, now that we've got our dependencies in order let's go build our markwon object and use it to apply markdown to a simple textview in our activity's layout file. The project template will have placed a textview in the activity's layout file and you'll just want to give it an id so that you can grab it via code.
val textView = findViewById<TextView>(R.id.markdown_text_view)
val prism4j = Prism4j(ExampleGrammarLocator())
val imagesPlugin = ImagesPlugin.create { plugin ->
plugin.addSchemeHandler(
NetworkSchemeHandler.create()
)
}
val theme = Prism4jThemeDefault.create()
val syntaxHighlightPlugin = SyntaxHighlightPlugin.create(prism4j, theme)
val markwon = Markwon.builder(this)
.usePlugin(imagesPlugin)
.usePlugin(syntaxHighlightPlugin)
.build()
markwon.setMarkdown(textView, markdownSample)
The first thing we need to create is our prism4j object which the syntax highlighter will use to parse code snippets. The prism4j object needs a GrammarLocator
and you'll see we are passing it our ExampleGrammerLocator
object. If you look in the project source code though you'll find that we don't have a class with that name anywhere.
This is where the prism4j bundler kapt dependency we added comes into play. We create an empty class that we apply the @PrismBundle
annotation to it. This annoation takes an array of languages we want to be able to parse, and then a name for an auto generated grammar locator class. I've included a link below to prism4j so that you can see what languages are supported. Since my blog posts often use kotlin and java, that's what I've added in the include array for our GrammarLocator.
@PrismBundle(include = ["kotlin", "java"],
grammarLocatorClassName = ".ExampleGrammarLocator")
class GrammarLocator { }
The next line of code in our configuration snippet sets up the image plugin so that our markwon object will be able to handle markdown that includes images. We're gonna need to load images via http so we have also made sure our image plugin is configured with the NetworkSchemeHandler
.
If you make use of Okhttp in your library, there is also an okhttp handler that can work nicely with your okhttp client. I won't go into that here though since for our simple example the default NetworkSchemeHandler
is enough.
Next we create a theme for our syntax highlighting. We're gonna just use the default theme, but rest assured you can customize and create your own themes.
After that we combine our prism4j object and our theme to create the syntax highlighting plugin object. We then use the markwon builder object and add all of the plugins we need in order to properly parse our markdown.
Finally we use the markwon object, our textview and our markdown sample string to display the mardown in our activity's layout.
If you run the app right now you'll see that everything rendered quite nicely, in fact at this point we've accomplished the goals we laid out at the start of this post. We've got markdown with images and code rendered natively in an android app. What else is there to do?
Well our code blocks suffer a little in the formatting department because of the width they are being constrained to, wouldn't it be nice if we could make the code snippets scroll horizontally? We've also thrown all of our markdown into a single textview and that's less than ideal for really long markdown documents.
So with that in mind, let's see how we can use a recyclerview to make this markdown look and behave even better.
The first thing we have to do is add a new dependency to the project. This additional markwon library will allow it to work with recyclerviews.
implementation 'io.noties.markwon:recycler:4.6.2'
Once we have that dependency in place we'll modify our activity layout to include a recyclerview and then add just a few lines of code to get us up and running.
val recyclerView = findViewById<RecyclerView>(R.id.markdown_recycler_view)
val markwonAdapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.default_markdown_layout)
.include(FencedCodeBlock::class.java, SimpleEntry.create(R.layout.code_block_layout, R.id.code_text_view))
.build()
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
recyclerView.adapter = markwonAdapter
markwonAdapter.setMarkdown(markwon, markdownSample)
markwonAdapter.notifyDataSetChanged()
We'll use the MarkwonAdapter
class's builder method to specify a default layout which has a textview at its root. This is the layout the markwon recyclerview will use to display markdown in by default. The next line is where we'll take advantage of some of of the magic markwon provides in its recyclerview. We'll specify that we want our code blocks to be rendered in a different layout and then provide the name of that layout to the SimpleEntry.create()
method. In this code specific layout we've got a textview that we've wrapped inside of a horizontal scrollview. This will allow our code blocks to keep their formatting and simply scroll beyond the width of the screen when necessary. We don't even have to worry about splitting up our markdown document ourselves, the library handles it all for us!
The rest of the code is just your standard setup of a recyclerview along with the setMarkdown
method call where you pass your markdown and your previously configured markwon object to the adapter. You do have to call one of the flavors of notifyDataSetChanged()
after you've set the markdown so that the adapter knows there's work to be done.
At this point you should be able to build and the run app to see your markdown rendered using a recyclerview with a special layout just for code blocks. This makes the document look much nicer and makes everything much better from a performance and memory stand point.
That's it for this post. If you need to display markdown in your native android app then I would strongly encourage you to checkout the Markwon library. It has a beautiful and comprehensive reference page and plenty of configuration options to suit whatever your use case may be.
Check out the links below for more info!
Useful Links
https://github.com/bltuckerdevblog/markdown-tutorial
https://github.com/noties/Markwon
https://github.com/noties/Markwon/tree/master/app-sample
https://noties.io/Markwon/docs/v4/image/
https://noties.io/Markwon/docs/v4/recycler/
https://noties.io/Markwon/docs/v4/syntax-highlight/
https://noties.io/Markwon/docs/v4/core/theme.html