Android (Kotlin)
The FeedbackJar Android SDK is a data layer plus an optional prebuilt board. Use just the data layer if you’re building your own UI, or drop in FeedbackJarView / FeedbackJarBoard for a complete list → detail → new feedback flow with voting and comments already wired up.
- Coordinates:
com.feedbackjar:sdk - Min SDK: 21 (Android 5.0)
- Repository: Maven Central · github.com/feedbackjar/kotlin-sdk
Coming from an iOS-only tool like WishKit? See the WishKit alternative for Android for a side-by-side comparison.
Installation
Add Maven Central (most projects already have it) and the dependency.
settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
} build.gradle.kts (app module):
dependencies {
implementation("com.feedbackjar:sdk:1.7.0")
} The SDK declares the INTERNET permission itself — nothing to add to your manifest.
Setup
Initialize once before use — typically in your Application.onCreate():
import com.feedbackjar.sdk.FeedbackJar
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
FeedbackJar.init(this, "your-widget-id")
}
} Get your widget ID from Settings → Widgets in your FeedbackJar project.
Prebuilt board (optional)
Skip building your own screens — the SDK ships a complete board on both view systems.
Views — no RecyclerView, no ConstraintLayout, no Material, no other AndroidX UI dependency:
import com.feedbackjar.sdk.ui.FeedbackJarView
import com.feedbackjar.sdk.ui.FeedbackJarActivity
// Full-screen, register-free
startActivity(FeedbackJarActivity.intent(this))
// optional accent:
startActivity(FeedbackJarActivity.intent(this, Color.parseColor("#e5484d")))
// Or embed the view directly
val board = FeedbackJarView(this)
board.setAccentColor(Color.parseColor("#e5484d")) // optional
setContentView(board) Compose — compileOnly in the published .aar, so it only resolves if your app already uses Compose:
import com.feedbackjar.sdk.ui.compose.FeedbackJarBoard
import androidx.compose.ui.graphics.Color
FeedbackJarBoard(
accentColor = Color(0xFFE5484D), // optional, defaults to FeedbackJar red
boardId = "board-id", // optional: restrict to one board
) Either one gives you the full loop — list, upvote, detail, comments, and a new-feedback form — built only on the API methods below. Nothing throws; failures render inline.
Identify the submitter (optional)
Remember a name/email so later submit, vote, and addComment calls attribute to the same person, and so their name/email are synced to the server against this install’s anonymous id:
FeedbackJar.setIdentity(name = user.name, email = user.email)
val identity = FeedbackJar.getIdentity()
// On logout:
FeedbackJar.clearIdentity() Skip this and submissions stay fully anonymous — setIdentity is optional, not required.
Verified identity (signed-in users)
If your app has its own signed-in users, attribute their votes/comments to a real, verifiable account — consistent across every device they use — instead of the unverified name/email above. Your backend computes the signature:
// Your backend, once the user is authenticated:
// timestamp = System.currentTimeMillis()
// signature = HMAC-SHA256(orgSecretKey, "$userId:$email:$timestamp")
// (org secret key is in the FeedbackJar dashboard)
FeedbackJar.setIdentity(
userId = user.id,
email = user.email,
signature = signatureFromYourBackend,
timestamp = timestampFromYourBackend,
) Once set, vote, addComment, submit, and hasVoted all attach to this real user on every device — immediately, and any votes/comments already made anonymously on this device are folded in automatically. timestamp must be the exact value your backend signed, and expires after 7 days — call setIdentity again on each sign-in.
Submit feedback
Each submission automatically includes device metadata (Android version, device model, screen size, app version, locale).
lifecycleScope.launch {
val result = FeedbackJar.submit(userText)
result
.onSuccess { response ->
Log.d("FeedbackJar", "Submitted: ${response.postId}")
}
.onFailure { error ->
Log.e("FeedbackJar", "Failed to submit", error)
}
} A callback variant is available when you are not using coroutines:
FeedbackJar.submit(userText) { result ->
result.onSuccess { response -> /* ... */ }
result.onFailure { error -> /* ... */ }
} Pass email/userName for a headless submission where identity isn’t already known, and properties for your own key/value context merged into the auto-collected app metadata:
FeedbackJar.submit(
userText,
properties = mapOf("plan" to "pro", "flavor" to BuildConfig.FLAVOR),
) List feedback
Fetch the public feedback feed with optional pagination:
lifecycleScope.launch {
val result = FeedbackJar.listFeedback(limit = 20)
result.onSuccess { page ->
page.posts.forEach { post ->
println("${post.title} — ${post.upvotes} upvotes")
}
}
} Pass boardId to filter by board, or cursor for pagination.
FeedbackJar.getPost(postId) fetches a single public post the same way.
Voting
Guest upvoting as this install’s anonymous guest — idempotent, and gated by the project’s Allow guest votes setting (WidgetConfig.allowVotes, see Get widget config):
lifecycleScope.launch {
FeedbackJar.vote(postId) // upvote
FeedbackJar.unvote(postId) // remove the upvote
val state = FeedbackJar.getVoteState(postId) // current count + hasVoted
} Comments
Anonymous, two-level threads — gated by Allow guest comments (WidgetConfig.allowComments):
lifecycleScope.launch {
val comments = FeedbackJar.listComments(postId, limit = 20)
// Reply to a post, or to a root comment via parentId (replies-to-replies aren't allowed)
FeedbackJar.addComment(postId, "Same issue here!", parentId = null)
} name/email fall back to the remembered identity (setIdentity) when omitted.
Get widget config
Check whether the org asks for name/email, and whether voting/comments are enabled, before building your own UI around them:
val config = FeedbackJar.getConfig().getOrNull()
if (config?.collectEmail == true) { /* show an email field */ }
if (config?.allowVotes == true) { /* show your vote button */ } | Field | Meaning |
|---|---|
collectName | Org asks submitters for their name (“Ask for Name”) |
collectEmail | Org asks submitters for their email (“Ask for Email”) |
allowVotes | Guest upvoting is enabled for this project |
allowComments | Guest commenting is enabled for this project |
API reference
| Method | Description |
|---|---|
init(context, widgetId) | Initialize the SDK. Call once before anything else. |
setIdentity(name?, email?) | Remember a submitter’s name/email for later calls. |
setIdentity(userId, email, signature, timestamp, ...) | Set a verified, org-signed identity — see Verified identity. |
getIdentity() | The currently remembered identity, if any. |
clearIdentity() | Forget the remembered identity (e.g. on logout). |
suspend submit(content, email?, userName?, properties?) | Submit feedback. Returns Result<FeedbackResponse>. |
suspend listFeedback(boardId?, limit?, cursor?) | List public feedback. limit is clamped to 1–50. |
suspend getPost(postId) | Fetch a single public post. |
suspend getConfig() | Fetch this project’s WidgetConfig. |
suspend vote(postId) / unvote(postId) | Cast or remove this install’s upvote. Idempotent. |
suspend getVoteState(postId) | Current upvote count and whether this install voted. |
suspend listComments(postId, limit?, cursor?) | List a post’s comments (two levels). |
suspend addComment(postId, content, parentId?, name?, email?) | Add a comment or reply. |
All suspend methods have a callback overload (callback: (Result<T>) -> Unit) safe to call from the main thread.
Notes
- Submission, voting, and comments are all anonymous by default —
setIdentityattaches a name/email, it does not require a full account or portal login. - Voting and commenting only work when the project has them enabled — check
getConfig()before showing that UI. - The server rate-limits submissions (5 per 15 minutes per IP).
- Private boards and non-public posts are never returned by
listFeedback.