Agent Template

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.

Android Kotlin APK App Starter
prfs Oct 06, 2026 1 use

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.

Topics Android Kotlin APK App Starter
Template Preview

Template Metadata

Slug
android-apk-starter-studio
Created By
prfs
Created
Oct 06, 2026
Usage Count
1

Tags

Android Kotlin APK App Starter

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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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("'", '&apos;')
    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', '')))