Android library for Pixiv OAuth 2.0 login (PKCE).
One dependency, three steps, done.
settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven("https://jitpack.io")
}
}build.gradle.kts
dependencies {
implementation("com.github.SoxiaLiSA:pixiv-login:1.2.0")
}This step is critical. Without it, the system cannot route the OAuth redirect back to your app.
Add an intent-filter to the Activity that will receive the login callback:
<activity
android:name=".LoginActivity"
android:launchMode="singleTask">
<!-- Pixiv OAuth callback -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- ⬇️ scheme must match the config you use ⬇️ -->
<!-- PIXIV_ANDROID → "pixiv" -->
<!-- PIXIV_COMIC → "pixiv-manga" -->
<data android:scheme="pixiv" />
</intent-filter>
</activity>| Config | Scheme | android:scheme |
|---|---|---|
PixivOAuthConfig.PIXIV_ANDROID |
pixiv |
"pixiv" |
PixivOAuthConfig.PIXIV_COMIC |
pixiv-manga |
"pixiv-manga" |
// ① Create client (singleton, keep it alive)
val client = PixivOAuthClient(PixivOAuthConfig.PIXIV_ANDROID)
// ② Start login — open the URL in a browser
val url = client.startLogin()
// Option A: Chrome Custom Tab (recommended)
CustomTabsIntent.Builder().build().launchUrl(context, url.toUri())
// Option B: WebView
webView.loadUrl(url)
// ⚠️ WebView cannot use Google/Apple/third-party login
// (Google blocks OAuth from WebView). Use Chrome Custom Tab
// if your users need to log in via Google.
// ③ Handle callback — in your Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent?) {
val result = client.tryHandleCallback(intent) ?: return
when (result) {
is PixivOAuthResult.Success -> {
val accessToken = result.response.accessToken
val refreshToken = result.response.refreshToken
val rawBody = result.rawBody // raw JSON for custom deserialization
// save tokens, navigate to main screen
}
is PixivOAuthResult.Failure -> {
// Structured error handling — no string matching needed
when (result) {
is PixivOAuthResult.Failure.MissingVerifier -> {
// Process died between startLogin() and callback
Log.w("Login", "Verifier lost, please log in again")
}
is PixivOAuthResult.Failure.ServerRejected -> {
Log.e("Login", "Server rejected: HTTP ${result.httpCode}")
}
is PixivOAuthResult.Failure.NetworkError -> {
Log.e("Login", "Network error", result.cause)
}
is PixivOAuthResult.Failure.MissingCode -> {
Log.e("Login", "No code in callback URI")
}
}
}
}
}
tryHandleCallbackdoes blocking I/O. Call it on a background thread or use the suspend version:
lifecycleScope.launch {
val result = client.tryHandleCallbackSuspend(intent) ?: return@launch
result.onSuccess { save(it.response.accessToken, it.response.refreshToken) }
.onFailure { Log.e("Login", it.message) }
}// The old refresh token is invalidated — always save the new one
val result = client.refreshToken(savedRefreshToken)
// or
val result = client.refreshTokenSuspend(savedRefreshToken)if (response.isExpired()) {
// token expired, call refreshToken
}
// Refresh 60 seconds early to avoid edge cases
if (response.isExpired(marginMillis = 60_000)) {
// almost expired
}| Chrome Custom Tab | WebView | |
|---|---|---|
| Google / Apple login | ✅ | ❌ Google blocks OAuth in WebView |
| Share browser cookies | ✅ | ❌ |
| UI customization | Limited | Full control |
| Requires Chrome | Yes | No |
Recommendation: Use Chrome Custom Tab. Pixiv supports Google login — if you use WebView, users who log in via Google will see an error page. Only use WebView if you are certain your users will only use Pixiv account + password.
The default in-memory verifier is lost if Android kills your app during login. To fix this, implement VerifierStore:
class MmkvVerifierStore(private val mmkv: MMKV) : VerifierStore {
override fun save(verifier: String) = mmkv.encode("pkce_verifier", verifier)
override fun load(): String? = mmkv.decodeString("pkce_verifier")
override fun clear() = mmkv.removeValueForKey("pkce_verifier")
}
val client = PixivOAuthClient(
config = PixivOAuthConfig.PIXIV_ANDROID,
verifierStore = MmkvVerifierStore(mmkv),
)By default, the client adds User-Agent, App-OS, X-Client-Time, and X-Client-Hash headers. If you supply your own header interceptor via baseClient, opt out:
val client = PixivOAuthClient(
config = PixivOAuthConfig.PIXIV_ANDROID,
baseClient = yourOkHttpClient,
addDefaultHeaders = false,
)val client = PixivOAuthClient(
config = PixivOAuthConfig.PIXIV_ANDROID,
logHttp = true, // DO NOT enable in production
)val client = PixivOAuthClient(
config = PixivOAuthConfig.PIXIV_ANDROID,
baseClient = yourOkHttpClient, // inherits your DNS, Chucker, etc.
)| Method | Description |
|---|---|
startLogin() |
Generate PKCE + return login URL |
tryHandleCallback(intent) |
Exchange code for tokens (if intent is a callback) |
refreshToken(token) |
Get new access token |
isExpired() / isExpired(margin) |
Check token expiry |
*Suspend variants |
Cancellation-aware coroutine versions |
minSdk 21- Kotlin or Java
MIT