You just got added to the repo. Your first task is to add an AI-powered image description button to the Android app. You plug in an endpoint, write a fetch, and it returns a sentence in the simulator. You open a PR. Then a senior engineer says, "What happens when the user switches Wi-Fi to LTE mid-request?" The success path is the easiest part. The failure path is where your work actually gets judged.
This article is a hands-on drill for that exact situation. You will build a small end-to-end AI feature using a free backend, then run it through a scripted mobile lifecycle test: backgrounding, network loss, app kill, and permission revoke. By the end, your first PR will include evidence that the feature survives reality, not just the simulator.
Why the Success Path Lies
A simulator gives you a stable network, a foreground activity, and no competition for system resources. Real users have airplane mode, app switchers, OS updates, and permission dialogs they accidentally tap wrong. Your AI feature might work perfectly in the demo and then silently fail when the request is interrupted. The fix is not to add more code blindly; it is to instrument the behavior and see where the request actually goes when the lifecycle changes.
For a junior engineer, this is also a way to build trust. A PR that includes a test script and a table of observed outcomes is easier to review than one that says "works on my machine." You are not just implementing a feature; you are proving it can fail safely.
The Free-Tier Setup That Makes This Drill Cheap
You do not need a corporate API account to run this experiment. MonkeyCode offers free model access and a free server option, which is enough for a small proxy and a handful of requests. That means you can practice the full flow without spending money or waiting for a manager to approve a vendor.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The free server is useful because it gives you a stable host for your proxy and lets you log every request that arrives from the phone. Without that log, you cannot tell whether the failure is on the device or in the network. With a simple proxy, you get a timestamped trace for each attempt.
A Reproducible Lifecycle Test Harness
Let's build a minimal stack. On the server side, run a tiny Express app. Replace MODEL_API_URL with the actual model endpoint you get from your free tier.
// server.js - run on your MonkeyCode free server
const express = require('express');
const app = express();
app.use(express.json());
app.post('/describe', async (req, res) => {
const { prompt } = req.body;
const started = Date.now();
console.log(`[AI_REQ] start ${started}`);
try {
const modelResponse = await fetch(MODEL_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const data = await modelResponse.json();
console.log(`[AI_REQ] end ${Date.now()}`);
res.json({ reply: data.reply, started, ended: Date.now() });
} catch (err) {
console.error(`[AI_REQ] error ${err}`);
res.status(502).json({ error: 'model unavailable' });
}
});
app.listen(3000);
On the Android side, use a simple React Native screen with a button that calls the proxy. The exact UI does not matter; what matters is that you add a unique AI_REQ log line on the device too, so you can compare device events with server logs.
const requestDescription = async (prompt: string) => {
console.log(`[AI_REQ] device start ${Date.now()}`);
try {
const response = await fetch('https://your-free-server.example.dev/describe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
console.log(`[AI_REQ] device end ${Date.now()}`);
return await response.json();
} catch (e) {
console.log(`[AI_REQ] device error ${e}`);
throw e;
}
};
Now you have two log streams. The server log tells you whether the request arrived. The device log tells you what the UI saw. When they disagree, you have found your bug.
The adb Drill
The real value comes from forcing state changes while a request is in flight. The easiest way to do this is with adb on a physical Android device. Here is a script you can save as lifecycle-drill.sh and run from your terminal.
#!/bin/bash
PACKAGE="com.example.ai.feature"
launch() {
adb shell am start -n $PACKAGE/.MainActivity
sleep 2
adb shell input tap 500 1000 # tap the fetch button
}
launch
sleep 3
echo "Baseline done. Check logs."
# Background / foreground
echo "--- Background / Foreground ---"
adb shell input keyevent KEYCODE_HOME
sleep 10
adb shell am start -n $PACKAGE/.MainActivity
sleep 2
adb shell input tap 500 1000
sleep 3
# Network loss mid-request
echo "--- Network Loss ---"
adb shell svc wifi disable
adb shell svc data disable
adb shell input tap 500 1000
sleep 5
echo "Re-enabling network..."
adb shell svc wifi enable
adb shell svc data enable
sleep 5
# App kill
echo "--- App Kill ---"
adb shell am kill $PACKAGE
adb shell am start -n $PACKAGE/.MainActivity
sleep 2
adb shell input tap 500 1000
sleep 3
echo "--- Collecting logs ---"
adb logcat -d | grep "AI_REQ"
The script does the same tap after each state change, which creates a fresh request. Your job is to watch how the app behaves: Does it show a spinner forever? Does it crash? Does it retry when the network comes back? The device log will tell you if the request was even attempted, and the server log will tell you if it ever arrived.
What to Record and Why
You need a table, not just a vague impression. For each condition, record the device model, Android version, network state, and exactly what the user sees. Then classify the recovery outcome into one of three buckets: recovered (the feature eventually succeeded after the condition cleared), restarted (the user had to trigger the action again), or silently disappeared (no error, no response, nothing).
Here is a decision table you can start with:
| Condition | Expected | Actual | Recovery |
|---|---|---|---|
| Baseline | Response in <5s | fill in | recovered |
| Background 10s | Response still returns | fill in | recovered |
| Network off mid-request | Timeout or error shown | fill in | restarted |
| App killed | No crash on relaunch | fill in | restarted |
| Permission revoked | Clear error message | fill in | restarted |
Fill it in as you run the drill. The empty cells are the point: you are generating evidence, not assumptions.
Limitations and Who Should Skip This
This drill has limits. It uses one device and one network emulation, so it will not prove anything about carrier-grade radio behavior or server load. A Wi-Fi switch on adb is not the same as a real cellular handover. Also, the free tier is not for production latency testing; if you need sub-second responses at scale, you need real instrumentation and a serious budget.
Do not use this approach if you are already past the proof-of-concept stage and are optimizing for p99 latency. This drill is for the first PR, when you are still validating that the architecture can handle a feature that falls down and gets back up.
Your First PR Can Prove Resilience
A junior engineer's first PR does not have to be a perfect implementation. It can be a testable implementation. With a free server and free model access from MonkeyCode, you can run this lifecycle drill on your own time and attach the results to your PR description. That turns a scary cold-start into a concrete, verifiable story: the request failed, the app recovered, and here is the timestamped proof.
Try this drill once, and answer honestly: what did your app do when the network came back?
Top comments (0)