To turn a dashboard or internal tool into an APK, bundle the chart library locally, cache the last API response in IndexedDB so the screen is never empty, queue form submissions while offline and send them when the connection returns, then build an APK and share it with staff directly or through a managed Google Play account.
Key takeaways
- Show cached data first, then refresh — never a blank screen on bad signal.
- Queue writes in IndexedDB and flush them on the
onlineevent. - Your API must allow CORS from
https://appassets.androidplatform.net. - Internal apps can be shared as an APK file — no Play listing required.
Charts that work offline
Chart.js (about 70 KB gzipped), uPlot (very small, very fast) and ECharts all run in a WebView. Download the minified file into vendor/ rather than using a CDN. Keep animations short on low-end phones — animation: { duration: 250 } in Chart.js.
Cache first, then refresh
async function load(url, key) {
const cached = localStorage.getItem(key);
if (cached) render(JSON.parse(cached), { stale: true }); // instant
try {
const r = await fetch(url, { headers: { Authorization: 'Bearer ' + token() } });
if (!r.ok) throw new Error(r.status);
const data = await r.json();
localStorage.setItem(key, JSON.stringify(data));
render(data, { stale: false });
} catch (e) {
if (!cached) showOffline();
}
}Show the time of the last successful update — staff trust a screen that says "Updated 08:14" more than one that might be old.
Offline data entry with a send queue
function submit(record) {
const q = JSON.parse(localStorage.getItem('queue') || '[]');
q.push({ ...record, id: crypto.randomUUID(), at: Date.now() });
localStorage.setItem('queue', JSON.stringify(q));
flush();
}
async function flush() {
let q = JSON.parse(localStorage.getItem('queue') || '[]');
while (q.length && navigator.onLine) {
const r = await fetch('/api/records', { method: 'POST', body: JSON.stringify(q[0]),
headers: { 'Content-Type': 'application/json' } }).catch(() => null);
if (!r || !r.ok) break;
q.shift(); localStorage.setItem('queue', JSON.stringify(q));
}
updateBadge(q.length);
}
addEventListener('online', flush);Sending a client-generated id lets the server ignore duplicates if a request succeeded but the response was lost. (Replace /api/records with your full https API address — inside the app a root path points at the app's own origin.)
CORS and authentication
Your files run on the origin https://appassets.androidplatform.net, so your API must return Access-Control-Allow-Origin for that origin (and handle preflight requests). Use token authentication in a header; cookies set by your API domain are third-party cookies from the app's point of view and are unreliable.
Getting it to staff
- Direct APK — share the file or a QR link (how to share an APK). Each phone must allow installs from the app used to open it.
- Managed Google Play — companies using Android Enterprise can publish private apps to their own staff only.
- Updates — keep the package name and signing key, raise the version code; Android upgrades in place and keeps the queue and cache.
Frequently asked questions
Can an HTML app talk to our company API?
Yes, over https, provided the API allows cross-origin requests from the app's origin and you use token authentication. Keep secrets on the server, not in the HTML.
Can an internal app be kept off the public Play Store?
Yes. Share the APK directly, or use managed Google Play to publish a private app only your organisation can see.