The short answerEvery app does two jobs: it draws pixels, and it asks the operating system for the rest. Only three things ever draw: the platform's toolkit, Flutter's engine, a browser engine. Who draws, and who talks to the OS, sorts the four families.
The film
TMIMI Explains
Who draws the pixels of a mobile app?
- the shelf
- the translator
- the painter
- the browser window
- the bridges
Film coming soon
Go deeper The film's words, and their real names 1 min
| In the film | Its real name |
|---|---|
| the system's workshop, its shelves of ready-made parts | the platform's toolkit: its views and widgets |
| the translator | React Native, turning your components into native views |
| Flutter's painter | Flutter's engine, Impeller |
| the canvas the system lends | one Android view, a surface: FlutterSurfaceView |
| the browser window the system lends | a WebView |
| the bridges | native modules and plugins |
| three painters | the three things that draw: the platform's toolkit, Flutter's engine, a browser engine |
Four families, three things that draw
Pick a family. Watch which layer draws, and how the app reaches the OS.
- Native. Your Kotlin or Swift calls the OS directly. The platform's own toolkit draws: Views or Jetpack Compose on Android, SwiftUI or UIKit on iOS. Bounds: a forest of boxes.
- React Native, with Expo. Your JavaScript runs in Hermes, with no HTML and no browser. It asks the OS for real native widgets, and the OS draws them. Bounds: a forest of boxes.
- Flutter. Your Dart code, and Flutter's own engine paints every pixel, on one surface the OS lends it. Bounds: one box.
- Hybrid: Ionic, Capacitor. Your web app runs full screen in a WebView, a browser engine inside a native shell. Plugins reach the OS. Bounds: one box, the WebView.
- PWA. A website with a manifest and a service worker, installed from the browser. The browser draws it and decides what the OS gives it. Bounds: Chrome's, not the app's.
Cross-platform is one family with two answers. React Native lets the OS draw. Flutter draws itself.
Go deeper What the explorer leaves out 1 min
Jetpack Compose is Google's toolkit for Android screens. It ships inside your app, not with the system. Its parts live inside one Android view, and Android's own graphics library draws them: that is why it counts as the platform's toolkit. Flutter brings its own engine.
With layout bounds on, Compose outlines its own parts too, so boxes cannot tell it from classic views. The view tree can, with AndroidComposeView.
Flutter keeps its own widgets on every phone. By default, it imitates each platform's fonts and scrolling. It can also embed a native view, such as a map, inside its surface.
Kotlin Multiplatform and .NET MAUI are cross-platform too. This course does not use them.
Hybrid is not cross-platform
Both promise one codebase for two platforms. The difference is who draws. In a hybrid app, a browser engine draws: the app is a web page. A cross-platform app has no browser.
React Native is not a web view: the OS draws its parts. Nor is Flutter: its own engine paints.
Go deeper Why a hybrid app can change without an update 1 min
On Android, the WebView is not part of your app. It is a system component: Android System WebView on Android 10 and later, Chrome itself on Android 7 to 9. Google updates it through the Play Store, on phones with Google services. On iOS, WKWebView ships with the OS.
So a hybrid app's rendering can change on a phone where nothing of the app was updated. A Flutter or Jetpack Compose app's widgets cannot: their code ships inside the app. A Views, UIKit, SwiftUI or React Native app's widgets change only when the OS does.
Checkpoint
Show layout bounds is on. An app shows one box the size of the screen, and nothing inside it. What can you conclude?
See the answer
One box says that one view holds the whole screen, and something paints inside it: a browser engine or Flutter's. Boxes are evidence; the view tree is the proof, step 4 of Try it.
Two answers decide the rest
The arguments you will hear are not opinions. Each follows from who draws and who talks to the OS. Pick a row.
| Native | Cross-platform | Hybrid | PWA | |
|---|---|---|---|---|
| Look | The platform's own. | React Native: the platform's widgets. Flutter: its own. | A web page, unless styled to the platform. | A web page, without browser bars. |
| Performance ceiling | The highest: nothing in between. | Close to native for most apps. | A browser engine's: long lists show it. | The same browser engine's limits. |
| Device access | Everything, the day the OS ships it. | Through modules, or your own native code. | Through plugins, or your own native code. | Only what the browser exposes; less on an iPhone. |
| Updates | A new build, through the stores. | The stores; JavaScript can also update over the air. | The stores; the web layer can update live. | The next visit. No store. |
Four consequences, two causes. When someone says one family is better, ask which row they mean.
Go deeper Updates that skip the store 1 min
Over the air. A React Native app can replace its JavaScript and images without a new store build: EAS Update does it for Expo. A hybrid app can do the same with its web layer. Both must stay within the stores' rules.
A PWA fetches its new version on the next visit. The new service worker waits, and takes over once the app's windows are closed.
With knowledge · in class
Name the family before the AI does
Ask an assistant for "a mobile app" and it chooses a family for you, often Expo. It will not ask you. The family is your first decision: name it, then check it.
Paste first, whatever the family
Before writing any file, state the family you chose and who will draw the pixels.
All families
Say it in the prompt
Put your family's phrase in your first prompt, after what the app does.
- Native Android:
a native Android app in Kotlin with Jetpack Compose, as an Android Studio project - Cross-platform:
an Expo (React Native) app in TypeScript - Hybrid:
an Ionic app with Capacitor, React, for Android and iOS - PWA:
an installable progressive web app with React and Vite: a web manifest and a service worker that works offline
Which family is this project?
- Is there a
package.jsonat the root? No: native, with Kotlin or Swift files, or Flutter if there is apubspec.yaml. Kotlin Multiplatform and .NET MAUI have none either; this course does not use them. - Is there a
capacitor.config.tsor.json, or a Cordovaconfig.xml? Yes: hybrid. - Does
package.jsonlistreact-native? Yes: cross-platform. No, with a web manifest and a service worker: PWA. Look for them as files, or as a plugin such asvite-plugin-pwainvite.config, which generates both at build time.
Native
Say it in the prompt
a native Android app in Kotlin with Jetpack Compose, as an Android Studio project
Put this phrase in your first prompt, after what the app does.
Then check the project
Expo
Say it in the prompt
an Expo (React Native) app in TypeScript
Put this phrase in your first prompt, after what the app does.
Then check the project
Ionic
Say it in the prompt
an Ionic app with Capacitor, React, for Android and iOS
Put this phrase in your first prompt, after what the app does.
Then check the project
PWA
Say it in the prompt
an installable progressive web app with React and Vite: a web manifest and a service worker that works offline
Put this phrase in your first prompt, after what the app does.
Then check the project
If the project contradicts your prompt, the assistant changed your decision: say so, and ask again.
Try it on your phone · 10 minutes
See who draws, on your own phone
Before class Turn on Developer options 1 min
Settings → About phone → tap Build number seven times. On a Samsung, it sits under Software information; on a Xiaomi, tap the HyperOS or MIUI version instead. Developer options then appear in Settings, often under System or Additional settings.
For step 4, also turn on USB debugging and install adb, as in capsule 02, Before class.
-
Turn it on. Developer options → Drawing → Show layout bounds.
This shows Outlines appear in every app, Settings included.
-
Open Settings, its home screen.

This shows A box around every row, icon and label. The platform's own toolkit draws this screen, part by part.
-
Open five apps you use, and sort them.
- A forest of boxes: Android views, Jetpack Compose or React Native.
- One box the size of the screen: another painter, Flutter or a WebView.
- A PWA you installed: Chrome draws it, so any boxes are Chrome's.
This shows A first sort. Boxes are evidence, not yet a proof.
-
With a cable, read the view tree. The command prints the top activity of every task: read the part under your app's
ACTIVITYline. Search it forWebView,FlutterView,AndroidComposeVieworReactSurfaceView.adb shell dumpsys activity topYou should see, for the hybrid demoACTIVITY com.tmimiexplains.webviewdemo/.MainActivity … View Hierarchy: com.android.internal.policy.DecorView{… 0,0-1080,2400}[MainActivity] android.widget.LinearLayout{… 0,0-1080,2337} android.widget.FrameLayout{… 0,136-1080,2337 android:id/content} android.webkit.WebView{… 0,0-1080,2201}This proves One view, a WebView. The browser engine draws everything inside it.
A store app may show short renamed classes instead, as the Flutter demo does in the fold below.
Go deeper Other trees, and Android Studio 3 min
Jetpack Compose draws its own outlines too: Settings → All apps shows boxes everywhere, and its tree ends in one AndroidComposeView. Boxes are evidence; the tree is the proof.
Its view tree, real output, trimmedACTIVITY com.android.settings/.spa.SpaActivity …
View Hierarchy:
com.android.internal.policy.DecorView{… 0,0-1080,2400}[SpaActivity]
android.widget.LinearLayout{… 0,0-1080,2400}
android.widget.FrameLayout{… 0,0-1080,2400 android:id/content}
androidx.compose.ui.platform.ComposeView{… 0,0-1080,2400}
androidx.compose.ui.platform.AndroidComposeView{… 0,0-1080,2400 …}
The Expo demo shows a ReactSurfaceView, then 7 ReactViewGroup and 10 ReactTextView: real Android views. Apps still on React Native's old architecture show ReactRootView.
The Expo demo, real output, trimmedcom.facebook.react.runtime.ReactSurfaceView{… 0,0-1080,2400 #1}
com.facebook.react.views.view.ReactViewGroup{… 0,0-1080,2400 #36}
com.facebook.react.views.view.ReactViewGroup{… 0,0-1080,420 #a}
com.facebook.react.views.text.ReactTextView{… 57,183-1023,289 #4}
com.facebook.react.views.text.ReactTextView{… 57,305-1023,362 #8}
…
The Flutter demo, a release build, shows one view the size of the screen with one child. R8 renamed both classes to y7 and p7. The build's mapping.txt translates them: FlutterView and FlutterSurfaceView.
The Flutter demo, real output, trimmedandroid.widget.FrameLayout{… 0,0-1080,2400 android:id/content}
y7{… 0,0-1080,2400 #1}
p7{… 0,0-1080,2400}
If the command hangs, a frozen app in the background can be the cause. Name your app's activity instead: adb shell dumpsys activity com.example.app/.MainActivity.
Android Studio's Layout Inspector shows the same tree as a picture, but only for a debuggable app: the debug builds you run from Android Studio, such as your native and Capacitor labs. Not Expo Go, which is a store app, and not a PWA, which runs inside Chrome.
Quick check
By the rule of this course, an assistant types most of your lab app's code. What stays yours?
See the answer
Every decision is yours, starting with the family you name in your first prompt, and every behaviour is tested by you. You test behaviours, not lines, and the language never decides the family: the drawing layer does.
An app is written in JavaScript, and its buttons are the platform's own buttons, drawn by the OS. Which family?
See the answer
The drawing layer decides the family, not the language: JavaScript that ends in real native widgets is React Native, cross-platform. A hybrid app would show one WebView, and JavaScript does not mean a web page.
Hybrid and cross-platform apps both promise one codebase for two platforms. What tells them apart?
See the answer
In a hybrid app a browser engine draws, so the app is a web page; a cross-platform app has no browser. React Native is JavaScript too, and hybrid apps ship through the stores as well.
Show layout bounds is on. Two cross-platform apps show the same screen. App A shows one box; app B, a box around every button. Which is which?
See the answer
Flutter paints its whole interface on one surface, so Android outlines one box; React Native asks the OS for real widgets, so each button gets its own. Same family, two hands on the pixels.
Show layout bounds is on. An app shows a box around every row, icon and label. What can you conclude?
See the answer
Classic views, Jetpack Compose and React Native all outline each part, so the boxes narrow it down and the view tree, from dumpsys, decides. Compose draws its own outlines, which is why boxes alone cannot prove classic views.
Your Ionic app must take a photo with the phone's camera. What carries the request from the page to the OS?
See the answer
In a hybrid app, plugins talk to the OS: Capacitor's Camera plugin is native code that the page calls from JavaScript. The WebView alone cannot open every door, and it runs with your app's permissions, not Chrome's.
Your app needs a phone feature that the browser does not expose. Which family cannot reach it, even with native code of your own?
See the answer
A PWA has no native code of its own: the browser decides what it gets, and gives less on an iPhone. A hybrid app adds plugins and React Native adds native modules: both can carry your own native code.
Overnight, the Play Store updated a phone's system components. Your app was not updated, yet one of its screens now looks different. Which app is it?
See the answer
On Android, the WebView is a system component, updated through the Play Store, so a hybrid app's rendering can change on its own. Flutter and Compose ship inside your app: their widgets change only with a new build.
You fix a bug in your PWA's JavaScript and deploy it to your server. How does the fix reach a user who installed the app?
See the answer
A PWA is a website: no store, and no build to review. The browser fetches the new service worker at the next visit, and it takes over once the app's old windows are closed.
You asked an assistant for an Ionic app with Capacitor. Its package.json lists expo and react-native, and there is no capacitor.config. What do you do?
See the answer
react-native in package.json means a cross-platform app, and a Capacitor project gets its capacitor.config at init, before any npx cap add. The assistant changed your decision: say so, and ask again.
Recap
- Two jobs. Every app draws pixels and asks the OS. Who does each defines the family.
- Native. The platform's toolkit draws. Your code calls the OS directly.
- Cross-platform. One codebase. React Native lets the OS draw; Flutter draws itself.
- Hybrid and PWA. A browser engine draws. Hybrid is not cross-platform.
- See it. Layout bounds give a first sort;
dumpsysgives the proof. - With an AI. Name the family first. Then check the project.
Sources
- React Native: Architecture glossary, host views: how JavaScript ends in native views.
- Flutter: Architectural overview, the engine that draws every pixel; Platform adaptations, the fonts and scrolling it imitates.
- Capacitor: Android documentation, the WebView it requires and which Android component provides it.
- web.dev: Learn PWA, manifest, service worker, installation; Vite PWA: guide, the plugin that generates both at build time.
- App Store Review Guidelines, 4.2 Minimum Functionality: a hybrid app must be more than a repackaged website.
- Chromium: WebView providers, who ships the WebView on Android 10 and later; Layout Inspector, which attaches to debuggable apps.
- androidx source: InnerNodeCoordinator, where Compose draws its own layout bounds when the option is on.