Pullboard
git 저장소에 존재하는 작업 보드로, 에이전트가 작업을 자동으로 할당·검토하고 별도 설명이 필요 없는 로컬 오픈소스 도구
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Deep linking without the fuss: the Android SDK for Lander. Read only mirror of sdk/lander_android.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Deep linking without the fuss. The native Android SDK for Lander, in Kotlin: receive App Links and custom scheme URLs, resolve them to the link you created in Lander, recover deferred links after install, create short links from inside the app, and report conversions.
Flow for every link: cold start, warm start, deferred (after install).DETERMINISTIC, PROBABILISTIC or UNATTRIBUTED.HttpURLConnection.Requires Android API 24+ (minSdk 24). Works from Kotlin and Java. Full docs: docs.golander.link/docs/android.
// app/build.gradle.kts
dependencies {
implementation("link.golander:lander-android:0.1.0")
}
The SDK declares the INTERNET permission and brings com.android.installreferrer:installreferrer, so there is nothing else to add.
Inside the <activity> that should open links (usually your launcher activity), in AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<!-- App Links for your Lander domain. autoVerify makes Android check
https://<domain>/.well-known/assetlinks.json, which Lander serves. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="acme.golander.link" />
</intent-filter>
<!-- Optional: custom scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="acmeshop" />
</intent-filter>
</activity>
android:launchMode="singleTop" makes links that arrive while the app runs go to onNewIntent instead of starting a second activity.
In the Lander dashboard (Apps, Android), add your package name and the SHA-256 fingerprints of every signing key (debug, upload and the Play App Signing key) so assetlinks.json verifies. Check verification with:
adb shell pm get-app-links com.acme.shop
Call initialize once, in Application.onCreate. It returns immediately: loading the install ID, the launch report (POST /v1/sdk/init) and, on first launch, the Play Install Referrer all run in the background.
class App : Application() {
override fun onCreate() {
super.onCreate()
Lander.initialize(
this,
apiKey = "pk_live_...", // publishable key from the dashboard
enableLogging = BuildConfig.DEBUG,
)
}
}
Only publishable keys (pk_test_..., pk_live_...) are accepted. Secret keys (sk_...) are rejected, because anything shipped in an app is public. Surrounding whitespace is ignored. A bad key throws IllegalArgumentException, and the message never contains the key.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Lander.handleIntent(intent)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
Lander.deepLinks.collect { link -> navigate(link.route) }
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
Lander.handleIntent(intent)
}
}
Call handleIntent in both places, for every intent: an intent without a link tells Lander the app was not opened by one. The SDK marks each intent it read, so an activity recreated after a rotation or process death does not deliver its link again.
LanderLink mirrors the API's ResolvedLink:
| Field | Meaning |
|---|---|
uri, uriString | The URL that opened the app, exactly as Android delivered it |
linkId | Lander link ID, or null when the link is unknown to Lander |
url | Canonical short link, for example https://acme.golander.link/p-123 |
path | The in app route set on the link, for example /product/123 |
params | Link params, with the opened URL's query parameters merged over them |
campaign, channel | Attribution labels set on the link |
isDeferred | True when the link was recovered after an install or handed off after sign in |
confidence | DETERMINISTIC, PROBABILISTIC or UNATTRIBUTED |
method | How the link was tied to the open: app_link, install_referrer, landing_page, authenticated_handoff or probabilistic_match |
isResolved | Whether Lander recognized the link |
route | path when resolved, otherwise the raw URL's path and query |
How delivery works:
deepLinks (cold start, or a stopped activity with repeatOnLifecycle) are kept and replayed, in order, to the next collector. You can even collect before initialize.deepLinks only (simplest), or call getInitialLink() for the launch link, for example to pick the first screen. Whichever gets it first wins; the other does not see it again, and later getInitialLink() calls return null.LanderLink with isResolved == false and only the URI set.acmeshop://product/123) are delivered as is, without a network call. route treats the host as the first path segment, so this one routes to /product/123. The exception is a scheme URL carrying a short link in lander_link: that link is resolved, and uri stays the scheme URL.// Option: decide the first screen from the launch link.
val initial = Lander.getInitialLink() // waits for the resolve call
getInitialLink waits until the launch intent reached handleIntent (or the first activity resumed), then for the resolve request when the app was opened by a link, and on a first launch for the init request (a deferred link may come back). Each request has a 10 second timeout; collecting deepLinks never blocks anything.
Opens are reported by the API when the SDK resolves the link, so you do not need to track them yourself.
Play Install Referrer, automatic and deterministic. The redirector sends the click to the Play Store with a single use token in the install referrer. On first launch the SDK sends the referrer to POST /v1/sdk/init, and the link comes back through getInitialLink / deepLinks with isDeferred == true, confidence == DETERMINISTIC and method == "install_referrer". Nothing to code. A link that opened the app wins over a deferred one.
Authenticated handoff (deterministic). Your server binds a link to a user, the app claims it after sign in:
val user = auth.signIn()
val link = Lander.claimHandoff(user.id) // null when nothing is pending
if (link != null) navigate(link.route)
claimHandoff returns the link and never also emits it on deepLinks, so navigation happens once. Use an ID that cannot be guessed (your auth system's user UUID, not an email address or a counter).
Everything else is UNATTRIBUTED: no deferred link is delivered, and the install is reported as unattributed.
try {
val link = Lander.createLink(
path = "/product/123",
params = mapOf("productId" to "123"),
title = "Trail runner",
channel = "share",
)
share(link.url)
} catch (e: LanderException) {
// e.code, e.statusCode, e.message, e.retryAfter
}
The slug is random and the domain is the project's default domain. The install ID is sent along so the API rate limits per install (20 links per minute). Requests are retried on network and server errors with the same Idempotency-Key, so a retry never creates two links. Retries wait at most 10 seconds in total: a longer Retry-After (and link_quota_exceeded) comes back as a LanderException with retryAfter instead.
Lander.trackEvent(
"purchase",
mapOf("amount" to 49.99, "currency" to "USD"),
linkId = link?.linkId, // optional: attribute to the link that opened the app
)
Fire and forget. Events are written to a queue on the device first and sent in batches of up to 100 to POST /v1/sdk/events:
Retry-After).trackEvent throws IllegalArgumentException for programming errors (empty name, name over 100 characters, values that are not JSON, a linkId that is not a UUID) and never throws for network errors.
Everything is available from Java through static methods, with callbacks on the main thread:
Lander.initialize(this, "pk_live_...");
Lander.handleIntent(getIntent());
LanderSubscription sub = Lander.addDeepLinkListener(link -> navigate(link.getRoute()));
Lander.createLink(
new LanderLinkOptions.Builder().path("/product/123").channel("share").build(),
new LanderCallback<LanderShortLink>() {
@Override public void onSuccess(LanderShortLink link) { share(link.getUrl()); }
@Override public void onError(Exception error) { }
});
Lander.trackEvent("signup");
getInitialLink(callback) and claimHandoff(userId, callback) work the same way.
Lander.installId is the random UUID v4 created on first launch. It is stored in noBackupFilesDir, so it is never restored onto another device, and it is gone when the app is uninstalled. If the API reports that the ID belongs to another project or environment (for example after switching from a pk_test_ to a pk_live_ key), the SDK creates a new one and reports a first launch.
claimHandoff (the API stores only a hash of it).noBackupFilesDir, excluded from backups.With the Lander monorepo running (pnpm dev), the API listens on port 8787. Use the seeded test key and the emulator's address for the host:
Lander.initialize(
this,
apiKey = "pk_test_DemoPublishableKey000000000000",
baseUrl = "http://10.0.2.2:8787",
enableLogging = true,
)
Plain HTTP needs a debug network security config that permits cleartext to 10.0.2.2 (the example app has one). Open a link without tapping one:
adb shell am start -a android.intent.action.VIEW -d "https://acme.golander.link/p-123" com.acme.shop
adb shell am start -a android.intent.action.VIEW -d "acmeshop://product/123" com.acme.shop
example/ is "Acme Shop", with a product screen at /product/:id. The seeded link https://acme.golander.link/p-123 resolves to /product/123. It shows what Lander resolved for each link, creates share links and tracks purchases.
./gradlew :example:installDebug
Point it at another API with -PlanderBaseUrl=https://api.golander.link -PlanderApiKey=pk_live_....
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./gradlew :lander:testDebugUnitTest :lander:lint :example:assembleDebug
./gradlew :lander:publishToMavenLocal # link.golander:lander-android:0.1.0 in ~/.m2
The SDK core (LanderClient) runs behind a LanderPlatform interface and an HttpTransport, so most tests run on the JVM with fakes and virtual time; Robolectric covers intents and storage.
Releases go to Maven Central from a monorepo tag android-vX.Y.Z, and sdk/lander_android is mirrored read only to github.com/SwiftwareIO/lander_android. See RELEASING.md for the one-time setup and the steps for each release.
The SDK only talks to Lander's public API (/v1/sdk/init, /v1/sdk/resolve, /v1/sdk/events, /v1/sdk/links, /v1/sdk/handoffs/claim), never to the database. Requests carry X-Lander-Sdk: android/<version>. Every request has a 10 second timeout; every write carries an Idempotency-Key that stays the same across retries.