Skip to content

Create your first extension

This guide covers the pieces that make an Android APK discoverable as a Katari extension.

Add the SDK

Add JitPack to dependency resolution in settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven(url = "https://www.jitpack.io")
    }
}

Add the Entry SDK as a compile-only dependency in the extension module:

dependencies {
    compileOnly("com.github.kiryl-kvit.katari:entry-source-api:sdk-2.0.1")
}

compileOnly is intentional. Katari supplies the API and its runtime dependencies when it loads the extension; packaging another copy in the APK can cause incompatible classes to be loaded.

The current SDK requires Android API 26 or newer. A typical extension module uses the following Android configuration:

android {
    namespace = "eu.kanade.tachiyomi.extension.all.example"
    compileSdk = 36

    defaultConfig {
        applicationId = "eu.kanade.tachiyomi.extension.all.example"
        minSdk = 26
        targetSdk = 36
        versionCode = 1
        // Entry API 2.0, extension revision 1.
        versionName = "2.0.1"
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

Use a stable, unique application ID. Changing it later makes Android and Katari treat the extension as a different installation.

Declare the extension

Add the extension feature and metadata to the module's AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-feature android:name="tachiyomi.extension" />

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="Katari: Example">

        <meta-data
            android:name="tachiyomi.extension.class"
            android:value=".ExampleFactory" />

        <meta-data
            android:name="tachiyomi.extension.nsfw"
            android:value="0" />
    </application>

</manifest>

The class name is resolved relative to the extension's application ID when it begins with a dot. Set tachiyomi.extension.nsfw to 1 when the extension exposes adult content.

Katari reads the Entry API family from versionName: the first two components are the API major and minor version, and the final component is the extension revision. For example, 2.0.1 targets Entry API family 2.0 and is revision 1 of this extension. Increase the final component for extension releases and keep versionCode monotonically increasing.

Create a factory

The manifest points to a public class with a no-argument constructor. The factory can return one source or several:

package eu.kanade.tachiyomi.extension.all.example

import eu.kanade.tachiyomi.source.entry.EntrySourceFactory
import eu.kanade.tachiyomi.source.entry.UnifiedSource

class ExampleFactory : EntrySourceFactory {
    override fun createSources(): List<UnifiedSource> = listOf(ExampleSource())
}

Keep the factory small. Request construction, parsing, filters, preferences, and media resolution belong in the source or in focused supporting files.

Create a source

This abbreviated image-source skeleton shows the required lifecycle:

package eu.kanade.tachiyomi.extension.all.example

import eu.kanade.tachiyomi.source.entry.EntryFilterList
import eu.kanade.tachiyomi.source.entry.EntryImageHttpSource
import eu.kanade.tachiyomi.source.entry.EntryMedia
import eu.kanade.tachiyomi.source.entry.EntryPageResult
import eu.kanade.tachiyomi.source.entry.EntryType
import eu.kanade.tachiyomi.source.entry.PlaybackSelection
import eu.kanade.tachiyomi.source.entry.SEntry
import eu.kanade.tachiyomi.source.entry.SEntryChapter

internal class ExampleSource : EntryImageHttpSource() {
    override val name = "Example"
    override val lang = "en"
    override val baseUrl = "https://example.com"
    override val supportsLatest = true

    override suspend fun getPopularContent(page: Int): EntryPageResult<SEntry> =
        error("Request and parse the popular catalogue")

    override suspend fun getLatestUpdates(page: Int): EntryPageResult<SEntry> =
        error("Request and parse the latest catalogue")

    override suspend fun getSearchContent(
        page: Int,
        query: String,
        filters: EntryFilterList,
    ): EntryPageResult<SEntry> = error("Request and parse search results")

    override suspend fun getContentDetails(entry: SEntry): SEntry =
        entry.copy().apply {
            initialized = true
            type = EntryType.MANGA
        }

    override suspend fun getChapterList(entry: SEntry): List<SEntryChapter> =
        error("Request and parse chapters")

    override suspend fun getMedia(
        chapter: SEntryChapter,
        selection: PlaybackSelection,
    ): EntryMedia = error("Return EntryMedia.ImagePages")
}

Catalogue entries should set type as soon as it is known; do not wait for the details request if the listing already provides enough information.

Continue with the Entry API guide for concrete entry and media payloads.

Test against a local Katari checkout

Publish the SDK from the Katari repository:

./gradlew --quiet :core:common:publishToMavenLocal :entry-source-api:publishToMavenLocal

Add mavenLocal() to the extension project's dependency repositories and temporarily use:

compileOnly("com.github.kiryl-kvit.katari:entry-source-api:local-SNAPSHOT")

Build the extension's debug APK with its Gradle module task. Install that APK on the same Android device as Katari, then review the source under Browse → Extensions. Katari will ask the user to trust an extension signed with an unfamiliar certificate.

Do not publish an extension that depends on local-SNAPSHOT; return to a tagged SDK version first.