Rich Text weergeven op Android met de Contentful SDK

Contentful is een CMS waarmee je eenvoudig content kan verspreiden naar allerlei platformen waaronder websites, iOS en android apps. Het voorziet een eenvoudig te gebruiken web app waar je je content kan beheren en doorsturen naar al je platformen. De content die je wil aanbieden, kan een van vele vormen zijn, elk met zijn eigen doel.

Rich Text

One of the content types is Rich Text. This allows you to format your text to give it more structure, add hyperlinks to external resources or even insert a code block or an image. Dit kan erg handig zijn voor inhoud waar veel tekst aanwezig is, zoals een blogpost of een artikel.

The Rich Text editor of the Contentful Web App

Als we de rich text willen weergeven op dezelfde manier dat we ze ingeven in de web app, dan hebben we een renderer nodig die alle verschillende stijlen aankan. In deze post gaan we kijken hoe de Contentful Android SDK deze data aanbied en hoe we deze mooi kunnen renderen.

Wat Voorbeeld Data Genereren

Voor we onze content kunnen aanspreken in de android app moeten we deze voorzien. In de Contentful Web App maken we een nieuw Blogpost content model. Ons model bevat een Rich Text veld genaamd body dat we kunnen gebruiken voor onze blogposts te schrijven. Daarna gaan we naar de content tab en schrijven we een kleine blogpost.

Onze kleine blogpost in de Contentful Rich Text Editor

Als laatste publiceren we het bericht en is onze voorbeeld content klaar.

De data uitlezen in Android

Om onze voorbeeld content te ontvangen in android gaan we gebruik maken van de officiële Contentful Android SDK.

Onze data verkrijgen is makkelijk. Allereerst moeten we de Content Delivery API Client initialiseren. Daarna kunnen we deze gebruiken voor onze blogpost op te vragen. We maken ook gebruik van rxAndroid om ons te helpen met de asynchrone netwerkoproepen.

// Building the CDAClient

val client = CDAClient.builder()
            .setSpace("{space-key-goes-here}")
            .setToken("{access-token-goes-here}")
            .build()

// Retrieving the sample blogpost

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({ entry ->
                val body = entry.getField<CDARichDocument>("body")
            }) {
                Log.e("Contentful Error", it.toString())
            }

Als we een kijkje nemen naar entry, kunnen we zien dat het ons body veldje bevat met daarin een CDARichDocument object. Als we dan onze body uitlezen met behulp van de getField methode, krijgen we een lijstje met objecten voor al onze elementen. Onze blogpost structuur is duidelijk te zien met headings, paragrafen, lijsten, hyperlinks, quotes en foto's allemaal in hun eigen object type.

De Rich Text Renderen

Nu dat we onze data hebben, moeten we deze weergeven. We zouden onze eigen mappers kunnen schrijven op zo de data te transformeren in views, maar dat zou heel veel tijd kosten. Gelukkig voor ons werkt het Contentful team zelf een een Rich Text Renderer voor Android.

Opgelet: Toen deze blogpost werd geschreven was deze library nog steeds in beta. De developers hebben wel onlangs gezegd dat ze de library productie klaar willen hebben in de komende weken.

Met deze library maken we een processor die ons toelaat om een CDARichDocument node te processen. We kunnen op twee manieren naar android renderen: via spannables of via custom views. We gaan beide manieren eens onder de loep nemen.

Spannables

Om met spannables te werken maken we gebruik van de sequenceProcessor. Deze gaat een SpannableStringBuilder genereren die we dan in een TextView kunnen gebruiken.

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({entry ->
                val body = entry.getField<CDARichDocument>("body")

                val context = AndroidContext(this)

                val sequenceProcessor = AndroidProcessor.creatingCharSequences()
                val sequenceResult = sequenceProcessor.process(context, body)

                textView.setText(sequenceResult, TextView.BufferType.SPANNABLE)

            }) {
                Log.e("Error", it.toString())
            }

En hier is het resultaat:

We kunnen onmiddellijk zien dat dit niet het resultaat is dat we verwachten. De meest opvallende problemen zijn:

  • Alles staat op één lijntje. Er zijn geen regeleinden waar we ze verwachten.
  • De foto wordt niet weergegeven.
  • Hoewel de hyperlink er uitziet als een link, is deze niet klikbaar.
  • Quotes worden niet weergeven zoals verwacht.
  • Het horizontaal lijntje is niet de hele breedte van het scherm.

With all these issues, this method is far from ideal.

Met al deze problemen is deze methode ver van ideaal.

Custom Views

De library voorziet ook een viewProcessor die een LinearLayout vol met custom elementen voorziet. Laten we deze eens uitproberen:

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({entry ->
                val body = entry.getField<CDARichDocument>("body")

                val context = AndroidContext(this)

                val viewProcessor = AndroidProcessor.creatingNativeViews()
                val viewResult = viewProcessor.process(context, body)

                viewResult?.setPadding(30, 30, 30, 30)
                container.addView(viewResult)

            }) {
                Log.e("Error", it.toString())
            }

Deze keer ziet het resultaat er veelbelovend uit:

Er zijn wel nog enkele problemen:

  • De foto wordt niet weergegeven.
  • De hyperlinks zijn deze keer klikbaar maar hebben wel een aparte stijl.

De stijl van de hyperlinks is interessant. Laten we is door de code graven om te kijken hoe we aan dit resultaat komen.

De hyperlink code analyseren

In de AndroidProcessor code zien we dat de viewProcessor gebruik maakt van een NativeViewsRendererProvider.

// AndroidProcessor.java

public class AndroidProcessor<T> extends Processor<AndroidContext, T> {

  ...

  public static AndroidProcessor<View> creatingNativeViews() {
    final AndroidProcessor<View> processor = new AndroidProcessor<>();
    new NativeViewsRendererProvider().provide(processor);

    return processor;
  }

  ...
}

Door in de NativeViewsRendererProvider te kijken, zien we dat deze een aantal verschillende renderers aanroept, inclusief eentje voor hyperlinks.

// NativeViewsRendererProvider.java

public class NativeViewsRendererProvider {
  
  public void provide(@Nonnull AndroidProcessor<View> processor) {
    processor.addRenderer(new TextRenderer(processor));
    processor.addRenderer(new HorizontalRuleRenderer(processor));
    processor.addRenderer(new ListRenderer(processor, new BulletDecorator()));
    processor.addRenderer(new ListRenderer(processor,
        new NumbersDecorator(),
        new UpperCaseCharacterDecorator(),
        new LowerCaseRomanNumeralsDecorator(),
        new LowerCaseCharacterDecorator(),
        new LowerCaseCharacterDecorator(),
        new UpperCaseRomanNumeralsDecorator()
    ));
    processor.addRenderer(new HyperLinkRenderer(processor));
    processor.addRenderer(new QuoteRenderer(processor));
    processor.addRenderer(new BlockRenderer(processor));
  }
}

In de HyperLinkRenderer kunnen we zien dat er een OnClick listener wordt toegevoegd die het effectieve doorlinken afhandelt. Ook zien we dat er een R.layout.rich_hyperlink_layout wordt ingeladen.

<!--rich_hyperlink_layout.xml-->

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#40808080"
    >

    <ImageView
        android:id="@+id/rich_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@null"
        android:padding="4dp"
        android:src="@android:drawable/ic_menu_share"
        />

    <LinearLayout
        android:id="@id/rich_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_toEndOf="@id/rich_image"
        android:gravity="center"
        android:orientation="vertical"
        />
</RelativeLayout>

Deze layout komt overeen met hoe de links er uitzien in de app. Indien deze stijl niet naar jouw wensen is, biedt de library een manier aan om stijlen toe te voegen of aan te passen.

Renderers toevoegen of overschrijven

De processor heeft twee methoden om renderers aan te passen: .addRenderer(…,…) en .overrideRenderer(…,…). Beide methoden verwachten twee parameters die elk een lambda zijn.

De eerste lambda heet de checker. Deze functie geeft je een node en verwacht een boolean terug. Hij checkt of deze node door deze renderer moet afgehandeld worden. (bv: Is deze node een hyperlink? of is deze node een foto?)

De tweede lambda is de effectieve renderer. Deze geeft je een node en verwacht een view terug. Deze tweede lambda gaat enkel aangeroepen worden indien de checker waar is.

Het verschil tussen addRenderer en overrideRenderer is wanneer ze aangeroepen worden. addRenderer gaat onderaan aan de lijst worden toegevoegd, terwijl overrideRenderer bovenaan de lijst staat. Elke node activeert enkel de eerste checker die overeenkomt.

Een eigen foto renderer toevoegen.

Laten we deze functies is uitproberen door een eigen renderer te schrijven die de foto's gaat laten zien. Zoals we al zagen in de body node, is een foto een CDARichEmbeddedBlock met daarin een CDAAsset. We moeten hier dus op controleren in onze checker. Om de effectieve foto in te laden gaan we Picasso gebruiken.

viewProcessor.overrideRenderer(
                    // Checker
                    { _, node -> node is CDARichEmbeddedBlock && node.data is CDAAsset },

                    // Renderer
                    { _, node ->
                        val data = (node as CDARichEmbeddedBlock).data as CDAAsset

                        val imageview = ImageView(this).apply {
                            layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                        }

                        Picasso.get().load("https:" + data.url()).into(imageview)

                        imageview
                    }
                )
We gebruiken hier overrideRenderer en niet addRenderer. Dit komt doordat onze foto node de checker van de default BlockRenderer activeert. Hierdoor zou onze renderer nooit aangeroepen worden indien we addRenderer gebruikten.

En als we kijken in onze app dan zien we dat nu de foto ook getoond wordt:

En dat is het!

We kunnen nu Contentful Rich Text velden weergeven in android en weten ook hoe we elementen kunnen toevoegen of overschrijven als we ze er anders willen laten uitzien.