Install
openclaw skills install @ahmadhussain0335/mobile-code-reviewReviews Kotlin, Java, Swift and Dart code and diffs in Android, iOS and Flutter apps for correctness, concurrency, lifecycle leaks, UI performance, architecture, error handling, tests, accessibility and i18n. Reports Blocker, Major, Minor and Nit findings with file:line and fix snippets, then an Approve or Request changes verdict. Use when reviewing a pull request, merge request, diff, branch or file before merging mobile code.
openclaw skills install @ahmadhussain0335/mobile-code-reviewReview mobile code the way a senior Android, iOS and Flutter engineer would: find the defects that will crash, leak, hang or mislead users in production, explain why each one matters, and show the fix.
Not for: a full security audit (use mobile-security), or designing an architecture from scratch
(use mobile-mvvm, mobile-mvi).
git diff --merge-base origin/main.| Severity | Meaning | Merge impact |
|---|---|---|
| Blocker | Crash, data loss or corruption, security or privacy breach, broken build, store rejection | Must fix before merge |
| Major | Wrong behaviour on a realistic path, leak, race, ANR or hang, swallowed error, missing test for new logic, broken layer boundary | Fix before merge, or defer only with a ticket |
| Minor | Maintainability, small inefficiency, unclear naming that misleads, missing edge-case test | Should fix, may be a follow-up |
| Nit | Preference or polish. Never blocks | Optional |
Rules for severity:
Work outlives its screen when launched in a global scope. It leaks memory and writes to dead UI.
Kotlin:
// BAD
GlobalScope.launch { repo.sync() }
// GOOD
viewModelScope.launch { repo.sync() }
Swift:
// BAD: unstructured task keeps running after the view disappears
.onAppear { Task { await model.load() } }
// GOOD: cancelled automatically when the view disappears
.task { await model.load() }
Dart:
// BAD
_sub = stream.listen(_onData); // never cancelled
// GOOD
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
Java:
// BAD: executor never shut down, callbacks hit a destroyed Activity
executor.execute(() -> textView.setText(load()));
// GOOD: work in a ViewModel, result exposed as LiveData, executor shut down in onCleared()
executor.execute(() -> result.postValue(load()));
Kotlin:
// BAD: runCatching also catches CancellationException and breaks structured concurrency
val user = runCatching { api.user() }.getOrNull()
// GOOD
val user = try {
api.user()
} catch (e: IOException) {
null
}
Swift:
// BAD: loop ignores cancellation
for id in ids { await upload(id) }
// GOOD
for id in ids {
try Task.checkCancellation()
await upload(id)
}
Kotlin:
// BAD: hardcoded dispatcher, untestable and easy to misuse
withContext(Dispatchers.IO) { dao.insert(item) }
// GOOD: injected dispatcher
class Repo(private val io: CoroutineDispatcher) {
suspend fun save(item: Item) = withContext(io) { dao.insert(item) }
}
Swift:
// BAD: UI state mutated from an arbitrary executor
final class FeedModel: ObservableObject { @Published var items: [Item] = [] }
// GOOD
@MainActor @Observable
final class FeedModel { var items: [Item] = [] }
Dart:
// BAD: 5 MB JSON parsed on the UI isolate, drops frames
final data = jsonDecode(body);
// GOOD
final data = await Isolate.run(() => jsonDecode(body));
Kotlin:
// BAD: Activity leaked for the lifetime of the process
object Analytics { lateinit var context: Context }
// GOOD
class Analytics @Inject constructor(@ApplicationContext private val context: Context)
Swift:
// BAD: self -> timer -> closure -> self
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.tick() }
// GOOD
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in self?.tick() }
Dart:
// BAD: controller created in build, never disposed
Widget build(BuildContext context) => TextField(controller: TextEditingController());
// GOOD: owned by the State object, disposed in dispose()
late final _controller = TextEditingController();
Dart:
// BAD
await repo.save();
Navigator.of(context).pop();
// GOOD
await repo.save();
if (!context.mounted) return;
Navigator.of(context).pop();
Kotlin (Compose):
// BAD: collects while the app is in the background
val state by viewModel.state.collectAsState()
// GOOD
val state by viewModel.state.collectAsStateWithLifecycle()
Kotlin (Compose):
// BAD: no key, item state jumps on reorder
LazyColumn { items(messages) { MessageRow(it) } }
// GOOD
LazyColumn { items(messages, key = { it.id }) { MessageRow(it) } }
Swift (SwiftUI):
// BAD: index identity, wrong rows animate on insert
ForEach(items.indices, id: \.self) { i in Row(item: items[i]) }
// GOOD: Item is Identifiable with a stable id
ForEach(items) { item in Row(item: item) }
Dart (Flutter):
// BAD: whole screen rebuilds on every tick
setState(() => _seconds++);
// GOOD: only the label listens
ValueListenableBuilder<int>(
valueListenable: _seconds,
builder: (context, value, _) => Text('$value'),
)
Kotlin:
// BAD
try { upload() } catch (e: Exception) { }
// GOOD
try {
upload()
} catch (e: IOException) {
_state.update { it.copy(error = UiError.Network) }
}
Swift:
// BAD
let data = try? await api.fetch()
// GOOD
do {
state = .loaded(try await api.fetch())
} catch {
state = .failed(error)
}
android.view, UIKit views or BuildContext.// BAD: ViewModel holds a View and returns a Retrofit DTO
class ProfileViewModel(private val view: TextView, private val api: Api)
// GOOD
class ProfileViewModel(private val getProfile: GetProfileUseCase) : ViewModel()
// BAD: real delay, flaky and slow
Thread.sleep(500)
// GOOD: virtual time, runs instantly
@Test fun retriesAfterBackoff() = runTest {
val repo = Repo(api = failingOnceApi, io = StandardTestDispatcher(testScheduler))
val result = async { repo.load() }
advanceTimeBy(500)
assertEquals(expected, result.await())
}
Swift: use await on the operation under test, not sleep. Dart: use fakeAsync or tester.pump(duration).
// BAD
Icon(Icons.Default.Delete, contentDescription = null) // actionable icon, no label
// GOOD
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete))
// BAD: concatenated String is shown verbatim, never localized, no plural rules
Text("You have " + String(count) + " messages")
// GOOD: localized key with a plural variation in the String Catalog
Text("You have \(count) messages", comment: "Inbox count")
@MainActorself checked for cycles; controllers and subscriptions disposedmobile-security)## Review: <MR title or scope>
Scope: <files / commits reviewed>. Platforms: <Android | iOS | Flutter>.
### Findings
1. [Blocker] path/to/File.kt:42 -- <one-line issue>
Why: <consequence for users or the codebase>
Fix:
```kotlin
<minimal snippet>
```
2. [Major] path/to/View.swift:88 -- <issue>
Why: ...
Fix: ...
### Questions
- path/to/file.dart:17 -- <thing that could not be confirmed from the code>
### Clean
- <dimensions checked with no findings, for example "Concurrency: no issues">
### Verdict
Request changes -- 1 Blocker, 1 Major.
Verdict rule: any Blocker or Major means Request changes. Otherwise Approve, listing Minor and Nit items as optional.
mobile-security -- deep MASVS audit when the change touches storage, network, auth or cryptomobile-kotlin, mobile-java, mobile-swift, mobile-flutter -- language rules the review appliesmobile-mvvm, mobile-mvi, mobile-design-patterns -- architecture expectationsmobile-merge-request -- MR description, commits and reviewer etiquettemobile-testing -- what test coverage a change should come withmobile-performance -- measuring a suspected jank, startup or memory regressionmobile-accessibility -- full accessibility audit against WCAG 2.2