Android APK Starter Studio
Turn an Android app idea into an actionable build plan and a downloadable, compilable Kotlin Android Studio starter project. Building the APK requires Android Studio or a local Android SDK.
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
Android APK Starter Studio is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Template Metadata
- Slug
- android-apk-starter-studio
- Created By
- prfs
- Created
- Oct 06, 2026
- Usage Count
- 1
Tags
Code Statistics
- HTML Lines
- 35
- CSS Lines
- 1
- JS Lines
- 85
- Python Lines
- 145
Source Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ agent_name }}</title>
<style>{{ css_code }}</style>
</head>
<body>
<div class="shell">
<header class="masthead"><div class="brand">📱 Android APK Starter Studio</div><p>From app idea to an Android Studio project you can build.</p></header>
<main class="workspace">
<section class="card form-card" aria-labelledby="project-heading">
<div class="eyebrow">PROJECT BRIEF</div><h1 id="project-heading">Shape your Android app</h1>
<form id="app-form">
<label for="app-name">Android app name</label><input id="app-name" maxlength="60" placeholder="e.g. Trail Companion" required>
<label for="package-name">Android package ID</label><input id="package-name" value="com.example.trailcompanion" spellcheck="false" placeholder="com.example.trailcompanion" required><small>Use lowercase dotted segments, such as com.example.trailcompanion.</small>
<label for="app-idea">What should your Android app do?</label><textarea id="app-idea" rows="5" maxlength="2200" placeholder="Describe who uses the app and the problem it solves…" required></textarea>
<label for="app-features">First Android features</label><textarea id="app-features" rows="3" maxlength="900" placeholder="One feature per line, e.g. Save a hiking checklist"></textarea>
<label for="app-style">Starter screen palette</label><select id="app-style"><option value="indigo">Indigo / sky</option><option value="forest">Forest / mint</option><option value="sunset">Sunset / peach</option></select>
<p id="form-error" class="error hidden" role="alert"></p>
<button id="generate-button" type="submit" disabled>Generate Android starter</button>
</form>
</section>
<section class="card result-card" aria-labelledby="output-heading">
<div class="result-header"><div><div class="eyebrow">BUILD DESK</div><h2 id="output-heading">Your Android build plan</h2></div><button id="download-button" type="button" class="secondary" disabled>⬇️ Download Android Studio project</button></div>
<div id="status" class="status" role="status">Initializing Android starter tools…</div>
<div id="results-container" class="results"><div class="empty"><span class="empty-icon">🧩</span><h3>Your Android idea starts here</h3><p>Generate a focused plan and a real starter project:</p><ul><li>📐 MVP screens and user flow</li><li>🛠️ Kotlin Android Studio project files</li><li>🎨 Starter screen with your app name and features</li><li>📦 Local instructions for building a debug APK</li></ul><p class="note">This browser does not compile or sign an APK. The starter is a UI scaffold, not a finished implementation of your features.</p></div></div>
</section>
</main>
</div>
<div id="loading-overlay" class="overlay hidden" aria-live="polite"><div class="loading-box"><span class="spinner" aria-hidden="true"></span><span>Drafting your Android build plan…</span></div></div>
<script>{{ js_code }}</script>
</body>
</html>
:root{--ink:#152244;--muted:#586781;--accent:#4355c9;--accent-dark:#3544a9;--canvas:#eef2fb;--paper:#fff;--line:#dce3f1;--danger:#a52639;--radius:18px;--shadow:0 16px 40px rgba(28,46,94,.09);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink)}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 95% 0%,#dce5ff 0,transparent 38%),var(--canvas);min-height:100vh}.shell{max-width:1430px;margin:auto;padding:32px 28px 64px}.masthead{margin-bottom:28px}.brand{font-size:1.55rem;font-weight:800;letter-spacing:-.035em}.masthead p{color:var(--muted);margin:8px 0 0}.workspace{display:grid;grid-template-columns:minmax(330px,450px) minmax(0,1fr);gap:22px;align-items:start}.card{background:var(--paper);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:28px}.eyebrow{font-size:.72rem;font-weight:800;letter-spacing:.16em;color:var(--accent)}h1,h2,h3{letter-spacing:-.035em}h1{font-size:1.6rem;margin:9px 0 23px}h2{font-size:1.45rem;margin:8px 0}h3{font-size:1.2rem}label{display:block;font-weight:700;margin:19px 0 8px;font-size:.91rem}input,textarea,select{width:100%;border:1px solid #cbd5e7;border-radius:11px;padding:12px 13px;color:var(--ink);background:#fff;font:inherit}textarea{resize:vertical}small,.note{font-size:.83rem;line-height:1.5;color:var(--muted)}small{display:block;margin-top:6px}button{font:inherit;font-weight:750;cursor:pointer;border:0;border-radius:11px;padding:13px 17px;background:var(--accent);color:#fff;transition:background .15s,transform .15s}button:not(:disabled):hover{background:var(--accent-dark);transform:translateY(-1px)}button:disabled{opacity:.48;cursor:not-allowed}#generate-button{width:100%;margin-top:22px}.secondary{background:#e9edff;color:#3343a8;font-size:.83rem}.secondary:not(:disabled):hover{background:#dce3ff}.result-header{display:flex;align-items:center;justify-content:space-between;gap:15px;flex-wrap:wrap}.status{color:var(--muted);font-size:.88rem;margin:14px 0;border-bottom:1px solid var(--line);padding-bottom:17px}.results{min-height:380px;line-height:1.72;overflow-wrap:anywhere}.plan{white-space:pre-wrap;font-family:inherit;margin:12px 0}.empty{padding:32px 8px}.empty-icon{font-size:2.5rem}.empty p{color:var(--muted)}.empty ul{padding-left:23px;line-height:2}.error{background:#fff0f2;color:var(--danger);padding:12px;border-radius:9px;margin-top:16px}.hidden{display:none!important}.overlay{position:fixed;inset:0;background:rgba(17,30,65,.45);display:grid;place-items:center;z-index:10}.loading-box{display:flex;align-items:center;gap:13px;background:#fff;padding:22px 28px;border-radius:16px;box-shadow:var(--shadow);font-weight:700}.spinner{width:22px;height:22px;border:3px solid #d9dfff;border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}:focus-visible{outline:3px solid #8293ff;outline-offset:3px}@media(max-width:1200px){.workspace{grid-template-columns:minmax(300px,390px) minmax(0,1fr)}.card{padding:22px}}@media(max-width:768px){.shell{padding:22px 14px 42px}.workspace{grid-template-columns:1fr}.result-header{align-items:flex-start}.secondary{width:100%}}
(() => {
const $ = id => document.getElementById(id);
const form = $('app-form');
const generateButton = $('generate-button');
const downloadButton = $('download-button');
const errorBox = $('form-error');
const overlay = $('loading-overlay');
const status = $('status');
const results = $('results-container');
let ready = false;
let busy = false;
let completedBrief = null;
const escapeHtml = value => String(value).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const localProvider = () => /local|wllama/i.test(String(window.PROVIDER || ''));
const modelAvailable = () => !localProvider() || !!(window.agentManager && window.agentManager.isLoaded);
function refresh() {
generateButton.disabled = busy || !ready || !modelAvailable();
downloadButton.disabled = busy || !ready || !completedBrief;
if (!ready) status.textContent = 'Initializing Android starter tools…';
else if (!modelAvailable()) status.textContent = 'Load a local model from the top bar to generate your Android plan.';
else if (!busy && !completedBrief) status.textContent = 'Ready to plan your Android app.';
}
function showError(message) { errorBox.textContent = message; errorBox.classList.remove('hidden'); }
function readBrief() {
return { name: $('app-name').value.trim(), packageName: $('package-name').value.trim(), idea: $('app-idea').value.trim(), features: $('app-features').value.trim(), style: $('app-style').value };
}
function validate(b) {
if (!b.name || !b.idea) return 'Give your Android app a name and describe what it should do.';
if (b.idea.length < 12) return 'Describe your Android app idea in at least 12 characters.';
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(b.packageName)) return 'Use a lowercase Android package ID such as com.example.myapp.';
return '';
}
window.addMessage = (type, content) => {
if (type !== 'assistant') return;
let live = results.querySelector('.live-plan');
if (!live) { results.innerHTML = '<pre class="plan live-plan"></pre>'; live = results.querySelector('.live-plan'); }
live.textContent += String(content);
};
document.addEventListener('pyodide-ready', () => { ready = true; refresh(); });
if (window.pyodideReady) { ready = true; }
refresh();
window.setInterval(() => { if (!ready && window.pyodideReady) ready = true; refresh(); }, 1000);
form.addEventListener('submit', async event => {
event.preventDefault();
const b = readBrief();
errorBox.classList.add('hidden');
const problem = validate(b);
if (problem) { showError(problem); return; }
if (!ready || !window.pyodide) { showError('Android starter tools are still initializing. Try again shortly.'); return; }
if (!modelAvailable()) { showError('Load a local model from the top bar before generating your Android plan.'); refresh(); return; }
busy = true; completedBrief = null; overlay.classList.remove('hidden'); status.textContent = 'Drafting your Android plan…'; refresh();
try {
const query = `App name: ${b.name}\nPackage ID: ${b.packageName}\nApp idea: ${b.idea}\nRequested features: ${b.features || 'Suggest a small MVP'}\nVisual palette: ${b.style}`;
window.pyodide.globals.set('user_query', query);
const answer = await window.pyodide.runPythonAsync('await process_user_query(user_query)');
results.innerHTML = '<pre class="plan">' + escapeHtml(String(answer)) + '</pre>';
completedBrief = b;
status.textContent = 'Plan ready. Download the Kotlin starter project to build an APK locally.';
} catch (err) { showError('Could not generate the Android plan: ' + (err && err.message ? err.message : String(err))); status.textContent = 'Android plan generation failed. Check your model or connection and retry.'; }
finally { busy = false; overlay.classList.add('hidden'); refresh(); }
});
downloadButton.addEventListener('click', async () => {
if (!completedBrief || !ready || busy) return;
const b = completedBrief;
busy = true; overlay.classList.remove('hidden'); status.textContent = 'Packaging your Android Studio starter…'; refresh();
try {
window.pyodide.globals.set('zip_name', b.name);
window.pyodide.globals.set('zip_package', b.packageName);
window.pyodide.globals.set('zip_idea', b.idea);
window.pyodide.globals.set('zip_features', b.features);
window.pyodide.globals.set('zip_style', b.style);
const encoded = String(await window.pyodide.runPythonAsync('_build_zip(zip_name, zip_package, zip_idea, zip_features, zip_style)'));
const binary = atob(encoded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const url = URL.createObjectURL(new Blob([bytes], {type:'application/zip'}));
const link = document.createElement('a');
link.href = url; link.download = (b.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') || 'android-app') + '-starter.zip';
document.body.appendChild(link); link.click(); link.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 60000);
status.textContent = 'Android Studio project downloaded. Follow README.md to build your debug APK.';
} catch (err) { showError('Could not package the Android project: ' + (err && err.message ? err.message : String(err))); }
finally { busy = false; overlay.classList.add('hidden'); refresh(); }
});
})();
import base64
import html
import io
import re
import zipfile
def _build_zip(name: str, package: str, idea: str, features: str, style: str) -> str:
if not re.fullmatch(r'[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+', package):
raise ValueError('Invalid Android package ID.')
if not name.strip() or not idea.strip():
raise ValueError('App name and idea are required.')
name = name.strip()[:60]
idea = idea.strip()[:2200]
lines = [line.strip()[:120] for line in features.splitlines() if line.strip()][:8]
if not lines:
lines = ['Customize your first screen', 'Build the features in your app plan']
palette = {
'indigo': ('#4355C9', '#F4F6FF'),
'forest': ('#206B53', '#F0FAF5'),
'sunset': ('#BD593F', '#FFF5EF'),
}
accent, background = palette.get(style, palette['indigo'])
esc = lambda text: html.escape(text, quote=True).replace("'", ''')
strings = ('<?xml version="1.0" encoding="utf-8"?>\n<resources>\n'
' <string name="app_name">' + esc(name) + '</string>\n'
' <string name="description">' + esc(idea[:450]) + '</string>\n'
' <string name="features">' + esc(' • '.join(lines)) + '</string>\n'
'</resources>\n')
activity = '''package __PACKAGE__
import android.app.Activity
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pad = (24 * resources.displayMetrics.density).toInt()
val panel = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(pad, pad, pad, pad)
setBackgroundColor(Color.parseColor("__BACKGROUND__"))
}
fun text(value: String, size: Float, color: String): TextView = TextView(this).apply {
this.text = value
textSize = size
setTextColor(Color.parseColor(color))
setPadding(0, 0, 0, pad)
}
panel.addView(text(getString(R.string.app_name), 32f, "__ACCENT__"))
panel.addView(text(getString(R.string.description), 18f, "#213047"))
panel.addView(text("STARTER FEATURES", 13f, "__ACCENT__"))
panel.addView(text(getString(R.string.features), 18f, "#213047"))
val action = Button(this).apply {
this.text = "Get started"
setOnClickListener {
Toast.makeText(this@MainActivity, "Connect your first feature here", Toast.LENGTH_LONG).show()
}
}
panel.addView(action)
panel.gravity = Gravity.CENTER_VERTICAL
setContentView(ScrollView(this).apply { addView(panel) })
}
}
'''.replace('__PACKAGE__', package).replace('__BACKGROUND__', background).replace('__ACCENT__', accent)
app_gradle = '''plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "__PACKAGE__"
compileSdk = 35
defaultConfig {
applicationId = "__PACKAGE__"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
}
'''.replace('__PACKAGE__', package)
manifest = '''<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="@string/app_name" android:theme="@android:style/Theme.Material.Light.NoActionBar" android:allowBackup="true">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
'''
readme = ('# ' + name.replace('\n', ' ') + '\n\n'
'This is a compilable Kotlin Android starter UI, not a finished implementation of the requested features. The Get started button is a placeholder.\n\n'
'## Build a debug APK\n'
'Install Android SDK Platform 35, JDK 17, and Gradle 8.9. From this project folder, run:\n\n'
' gradle wrapper --gradle-version 8.9\n'
' ./gradlew assembleDebug\n\n'
'On Windows, use gradlew.bat assembleDebug. Alternatively, open the folder in Android Studio, configure Gradle 8.9, sync, and choose Build > Build APK(s).\n'
'Output: app/build/outputs/apk/debug/app-debug.apk\n\n'
'The generated ZIP intentionally does not include a Gradle wrapper JAR; the first command creates the wrapper.\n'
'The project needs an Android SDK and access to Gradle plugin dependencies for its first build.\n\n'
'## Next steps\nImplement the requested screens, persistence, permissions, and any integrations before releasing. For distribution, configure signing and create a release build.\n')
files = {
'settings.gradle.kts': 'pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }\ndependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } }\nrootProject.name = "AndroidStarter"\ninclude(":app")\n',
'build.gradle.kts': 'plugins {\n id("com.android.application") version "8.6.1" apply false\n id("org.jetbrains.kotlin.android") version "2.0.21" apply false\n}\n',
'gradle.properties': 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\nandroid.useAndroidX=true\n',
'app/build.gradle.kts': app_gradle,
'app/src/main/AndroidManifest.xml': manifest,
'app/src/main/java/' + package.replace('.', '/') + '/MainActivity.kt': activity,
'app/src/main/res/values/strings.xml': strings,
'README.md': readme,
}
memory = io.BytesIO()
with zipfile.ZipFile(memory, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
for path, content in files.items():
archive.writestr(path, content)
return base64.b64encode(memory.getvalue()).decode('ascii')
async def process_user_query(query: str) -> str:
"""Draft an Android implementation plan for the supplied app brief.
Args:
query: App name, package, idea, features, and palette.
Returns:
An actionable Android implementation plan.
"""
prompt = ('Plan this Android app for a beginner. The ZIP download is a basic Kotlin Activity UI scaffold with app title, description, feature labels, and a placeholder button. It does NOT implement the described features. Do not say an APK has been compiled. Reply in plain text, concise, with headings: MVP, Screens, Data and permissions, Build next. Avoid unsupported promises.\n\nApp brief:\n' + query[:3500])
return str(await agentop_llm.generate(prompt, globals().get('TEMPLATE_SYSTEM_PROMPT', '')))
Name your agent
Based on . You'll get your own private copy to try and customize.