Download

Start

Set up a project

One Gradle project builds one extension APK.

Gradle files #

An extension is an ordinary Android app project. Build it with JDK 21, the Android SDK and the Gradle 9.4 wrapper, and put its code in src/main/kotlin.

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

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

rootProject.name = "example-comics"
gradle.properties
android.useAndroidX=true
kotlin.stdlib.default.dependency=false
build.gradle.kts
plugins {
    id("com.android.application") version "8.13.2"
    id("org.jetbrains.kotlin.android") version "2.3.20"
    id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20"
}

android {
    namespace = "org.example.katari.examplecomics"
    compileSdk = 36

    defaultConfig {
        applicationId = "org.example.katari.examplecomics"
        minSdk = 29
        targetSdk = 36
        versionCode = 1
        versionName = "4.0.0"
    }

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

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

dependencies {
    compileOnly("com.github.pa2x2.katari:entry-source-api:sdk-4.0.0")
}
  • Keep the SDK compileOnly, and the libraries below too if you name them yourself, so the APK holds only your own code. Katari supplies all of them.
  • kotlin.stdlib.default.dependency=false leaves Kotlin's standard library out of the APK as well. Katari always loads kotlin.* from its own copy, so a bundled one would only take space.
  • Katari runs on Android 10 and newer, which is API level 29, and on Linux, where it converts the same APK.

What Katari provides at runtime #

Katari's release build keeps these libraries for extensions, in the versions of the current release:

Library Version Packages
Kotlin standard library 2.4.21 kotlin.*
kotlinx.coroutines 1.11.0 kotlinx.coroutines.*
kotlinx.serialization: JSON and Protobuf 1.12.0 kotlinx.serialization.*
kotlinx-datetime 0.8.0 kotlinx.datetime.*
OkHttp, with its logging, Brotli, Zstandard and DNS over HTTPS modules 5.5.1 okhttp3.*
Okio 3.18.2 okio.*
Jsoup 1.24.1 org.jsoup.*
Injekt 1.16.1 uy.kohesive.injekt.*, as Services describes
RxJava 1, for the SDK's deprecated Observable calls 1.3.8 rx.*
AndroidX Preference, for settings 1.2.1 androidx.preference.*

The SDK brings the first two, JSON, OkHttp, Okio, Jsoup, Injekt, RxJava and Preference to your compile classpath. Add Protobuf or kotlinx-datetime yourself with compileOnly, in the version above or older.

Of the SDK itself, use eu.kanade.tachiyomi.source.*, eu.kanade.tachiyomi.network.* and app.katari.book.api.*. Its other packages, and libraries it depends on but that aren't listed above, such as logcat, are Katari's own code: they compile, but its release build may have removed or changed them, and the extension then fails with NoClassDefFoundError or NoSuchMethodError only in users' copies of Katari. On Linux, an Android class or member Katari doesn't provide there shows as Not on desktop yet instead. Log with android.util.Log.

The manifest #

src/main/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 Comics">

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

        <meta-data
            android:name="tachiyomix.contentWarning"
            android:value="0" />
    </application>
</manifest>
Entry What Katari does with it
tachiyomi.extension feature Marks the app as an extension. Without it Katari ignores the APK.
tachiyomi.extension.class Required. The classes Katari creates, separated by ;. Each is an EntrySourceFactory or a single source implementing UnifiedSource, with a public constructor that takes no arguments, so a Kotlin object won't do. A leading dot puts the applicationId in front, not the namespace.
tachiyomix.contentWarning 0 for none, 1 when some titles are for adults only, 2 when all of them are. Any other value counts as 0.
android:label The extension's name in the list, with a leading Katari: , Tachiyomi: or K-Mihon: left out.
tachiyomix.name Optional. A name for the list that differs from android:label, which the launcher and Android's settings show.
tachiyomix.extensionLib Optional. The SDK as 4.0, when the versionName doesn't start with it. Leave it out and follow Version numbers.
android:icon Shown beside the extension once it is installed. Give a 192 px square as res/mipmap-xxxhdpi/ic_launcher.png and smaller copies for the other densities.

An exception thrown while creating any of the classes leaves the whole extension under Not loaded, as What users see before it runs lists.

Version numbers #

  • versionName is the SDK's major and minor, then your own release number: 4.0.0, 4.0.1 and so on. When you move to a newer SDK, start that one's count at 0.
  • versionCode goes up by one with every release you publish. Stores offer an update when it grows, or when the listing is built with a newer SDK. Android refuses to install a lower one, and a user's Trust holds for one version code.
  • applicationId never changes once published. It is how Android and Katari know a new APK updates the old one; another ID installs as a second extension.

Try it on a phone Android #

  1. Build the debug APK and install it with a phone connected:
    Shell
    ./gradlew assembleDebug
    adb install build/outputs/apk/debug/example-comics-debug.apk
  2. Open Browse › Extensions. The extension is listed as Untrusted; tap the shield and Trust.
  3. Its sources appear under Browse › Sources.

If the extension is already installed from a store, Android refuses a copy signed with another key, so uninstall that one first. With Private picked as the installer, Katari keeps both copies and uses the one with the higher versionCode, and yours on a tie. If it lands under Not loaded instead, tap the info button for the reason, as What users see before it runs lists. An exception a source throws shows as an error where the user was, such as on the source's list or the title's page, and Katari logs most of them with their stack trace to adb logcat.