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:
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:
Add mavenLocal() to the extension project's dependency repositories and temporarily use:
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.