# I built a TikTok-style feed in DartNative. The native parts held up, the launch did not

> DartNative shipped PageView four days after my video feed comparison, so I rebuilt the feed as a TikTok clone with Liquid Glass buttons, AirPlay, Picture in Picture and lock-screen controls, then profiled it on an iPhone. The UI is real UIKit. Launch took three seconds, and the trace says why.

- Author: Sailesh Dahal (https://saileshdahal.com.np)
- Published: 2026-09-24
- Updated: 2026-09-24
- Reading time: 17 min
- Canonical: https://saileshdahal.com.np/dartnative-tiktok-feed-native-features
- Tags: Flutter, Dart, Mobile Development

---

*Update: with 30 pages instead of 1,000, the same screen starts in about 0.35 seconds. With 10,000 it never starts: iOS kills it for memory. `PageView.builder` here is not lazy. [The numbers are below](#update-30-pages-instead-of-1000-and-10000).*

Eleven days ago I [built the same infinite video feed in DartNative and Flutter](/dartnative-vs-flutter-infinite-video-feed) and spent a whole section on the fact that DartNative had no `PageView`. I got paging working with 22 lines of Objective-C and two workarounds nobody had written down. On 17 September DartNative shipped `PageView`, `PageController` and a `pagingEnabled` flag on `FastList`, and its playground gained an infinite video feed demo that looks a lot like mine.

So I went back. This time I did not stop at a feed. I built the screen people actually mean when they say "a video feed": TikTok's. Following and For You tabs, a column of Liquid Glass buttons down the right edge, a comments sheet with a real text field, swipe left for the creator's profile, a floating glass tab bar, pull to refresh, AirPlay, Picture in Picture, audio that keeps playing with the phone locked, and next and previous on the lock screen. Then I put it on a cabled iPhone 16 Pro Max running iOS 27.0, profiled it, and kept the logs.

The short version: every native feature worked, and most of them cost me nothing. The ones that did cost me a 744-line plugin. And the finished screen took three seconds to show its first frame, where the plain feed took half a second.

## PageView landed, and the shim is gone

The 22 lines of Objective-C, the safe-area workaround and the manual `DynamicLibrary.process()` lookup are all deleted. The feed is `PageView.builder(scrollDirection: Axis.vertical, ...)` and nothing else, and one swipe is one page on the platform's own paging.

The item ceiling from the last post is still there. It is documented now rather than fixed. The doc comment on `PageView` in the SDK says an unbounded builder grows "up to a ceiling of about a thousand full-screen pages on a phone, past which the list ends: native paging stops snapping beyond that much content." The release notes describe the same builder as a feed that "has no end and grows as the reader scrolls." Those two sentences do not describe the same widget, and only the doc comment matches what I found in September.

I also pulled 10,000 portrait clips from the Pexels API and hard-coded them, because a TikTok clone with 17 videos is a slideshow. They added 3.3MB to the app. The pager has 1,000 pages, so one session never sees more than a thousand of them.

## What DartNative gave me for free

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(216px,1fr));gap:0.75rem;align-items:start">

![The TikTok-style feed on an iPhone 16 Pro Max: Following and For You tabs at the top, a column of Liquid Glass buttons for like, comment, save, share, AirPlay and Picture in Picture on the right, the caption at the bottom left, a progress bar, and a floating Liquid Glass tab bar.](https://saileshdahal.com.np/images/posts/dartnative-tiktok-feed-native-features/feed.png)

![The comments sheet: a native iOS sheet with a grabber, six comments, and a text field in a glass capsule sitting on the keyboard, with the Send return key and QuickType suggestions.](https://saileshdahal.com.np/images/posts/dartnative-tiktok-feed-native-features/comments-keyboard.png)

![A creator profile reached by swiping left: a glass-ringed avatar, follower counts, a red Follow button, and a grid of the creator's clips. The avatar and two tiles are black because their posters never rendered.](https://saileshdahal.com.np/images/posts/dartnative-tiktok-feed-native-features/profile.png)

</div>

*Left to right: the feed, the comments sheet with the keyboard up, and a creator's profile after swiping left. All three are screenshots from the iPhone.*

This is where DartNative is at its best, and it is not close. Every control on these screens is the platform's own:

- The buttons on the right are `GlassEffectContainer`, which is iOS 26 Liquid Glass sampling the video under it, with the system press animation when `interactive` is set.
- The tab bar is `BottomNavigationBar`, which on iOS 26 renders as the real floating `UITabBar` pill.
- Share opens `UIActivityViewController` through the `dartnative_share` plugin.
- The progress bar is a `UIProgressView`, indeterminate while a clip loads.
- Likes and saves fire `HapticFeedback`.

The comments sheet is `showModalSheet`, which is `UISheetPresentationController` with its detents, grabber and swipe to dismiss. The field is a `UITextField`: QuickType, the loupe, the Send return key from `TextInputAction.send`, all of it. The composer sits in `Scaffold.bottomInputBar` and rides the keyboard's own animation. I wrote no native code for any of this.

The middle screenshot also shows a bug. The "4.9K comments" title is dark text on a dark sheet: the native title follows the phone's light theme, not the sheet colour I set.

## What it did not give me cost 744 lines

`dartnative_video_player` 1.0.1 gives you play, pause, seek, volume, speed, looping, fit and a pre-cache. It has no API for AirPlay, Picture in Picture, background playback, or the lock-screen player. The framework has no pull to refresh at all. I searched the SDK, the docs and the playground for `RefreshIndicator`, `onRefresh` and `UIRefreshControl` and found nothing.

So I wrote them as a plugin: 450 lines of Swift and 294 of Dart. It follows DartNative's plugin guide properly, with `@_cdecl` entry points, a claimed view type and the dispatcher slot for calling back into Dart. The guide is good. The starting point is not: `dn create --template=plugin` generates Flutter's method-channel plugin template, with `MethodChannel` and `plugin_platform_interface`, which is the opposite of what the guide tells you to build.

### How the plugin reaches the player

The video plugin is closed source and exposes one thing about its native side, `nativeViewPtr`, a pointer to the view it draws into. My plugin walks that view's layer tree until it finds an `AVPlayerLayer`, takes its `AVPlayer`, and hangs Picture in Picture, AirPlay and the lock-screen controls off it. Pull to refresh works the same way: it walks the window for the visible scroll view that pages vertically and attaches a `UIRefreshControl` to it. Both work today. Both depend on internals nobody promised me, and the first plugin update that wraps its layer differently breaks them without a compile error.

### What worked on the phone

I tried every one of these on the phone and they all worked: AirPlay to another screen, Picture in Picture from the button and from swiping home, audio with the screen locked, next and previous on the lock screen and in Control Center, pull to refresh, tapping Home to jump to the top and refresh, the comments sheet, the share sheet and the profile swipe. That is a manual check. I have no timings for any of it.

The profile screenshot on the right shows the other bug I did not chase: some avatars and grid tiles never render their poster, although the same URLs return a normal JPEG to `curl`.

### Three players or four

`VideoPlayerController`'s documentation says to keep at most three players alive, because a phone has roughly three hardware decoders. The playground's own feed demo keeps four on iOS.

## What this screen would take in Flutter

I did not build the Flutter version, so this section compares against what Flutter's own packages offer today, with no platform code of my own, and against the Flutter issues where people have been asking for the missing parts. Issue ages and comment counts are as of 24 September 2026.

"Without method channels" is not the difference. My plugin has no `MethodChannel` in it, but Flutter apps can skip method channels too: `dart:ffi` and the Dart team's `ffigen` and `objective_c` packages reach the same Objective-C and Swift APIs. The difference is what that native code can find when it gets there. In DartNative the video, the glass and the text field are UIKit views in one hierarchy, so a few lines of Swift can walk to the `AVPlayerLayer` and hand it to AVKit. In Flutter the video is a texture the engine composites, and there is no layer to hand over.

### The open issues, oldest first

Each entry is a Flutter issue behind something on this screen, in the order it was opened, with what DartNative does about it.

#### November 2017: autocorrect on iOS

[#12920](https://github.com/flutter/flutter/issues/12920), "Autocorrect tooltips don't appear on iOS", is still open with 65 comments. Flutter draws its own text field on top of the system's text input, so the parts of iOS text editing it has not rebuilt are simply missing. The comment field in this app is a `UITextField` and does whatever iOS does.

#### February 2019: no video cache

[#28094](https://github.com/flutter/flutter/issues/28094) asks for `video_player` to cache anything at all. It has 119 comments and 140 thumbs up. DartNative's player shipped with a pre-cache and a disk cache, and the September post found that cache was a large part of why its feed loaded faster.

#### April 2019: PageView does not preload

[#31191](https://github.com/flutter/flutter/issues/31191), "PageView should preload pages", 49 comments and 130 thumbs up. DartNative's `PageView` has the opposite problem, as the next section shows.

#### July 2019: sheets that fight their own scroll

[#36283](https://github.com/flutter/flutter/issues/36283), "Can't swipe to dismiss scrollable Bottom Sheet", 46 comments. Flutter now has `CupertinoSheetRoute`, but it is still Flutter drawing a sheet. The comments sheet here is `UISheetPresentationController`, with the system's own detents and dismiss gesture.

#### May 2020: the keyboard and the scroll

[#57609](https://github.com/flutter/flutter/issues/57609), "Let iOS keyboard dismissal synchronize with scroll", 53 comments and 94 thumbs up. Same root as 2017: the keyboard belongs to iOS, the field and the scroll view belong to Flutter, and the two have to be kept in step by hand. In DartNative all three are UIKit.

#### June 2020: AirPlay and Picture in Picture

[#59554](https://github.com/flutter/flutter/issues/59554) asked for AirPlay and Chromecast in the video player. It was closed as not planned in July 2023.

A week later [#60048](https://github.com/flutter/flutter/issues/60048) asked for Picture in Picture on iOS. It is still open, with 49 comments, and the first post says why it is hard: `video_player` draws video into a texture, and iOS only does Picture in Picture from an `AVPlayerLayer`. My plugin needed native code for this too, but the plugin's video is already an `AVPlayerLayer` in the view tree, so there was something to hand to AVKit. In Flutter, the player would have to change how it draws video first.

#### July 2020: how do you build this screen at all

[#62265](https://github.com/flutter/flutter/issues/62265), "How to effectively implement a feed of Video Players in Flutter", 79 comments. It is the question this whole series started from.

#### June 2025: Liquid Glass

[#170310](https://github.com/flutter/flutter/issues/170310), "Support for iOS 26 Liquid Glass Design in Cupertino Widgets", is the youngest issue here and the loudest: 87 comments, 557 thumbs up, labelled P3. Flutter draws its own UI, so it would have to imitate the material. DartNative's `GlassEffectContainer` is the material, sampling the video under it.

#### September 2026: AirPlay in Flutter, PageView in DartNative

On 4 September [flutter/packages#12490](https://github.com/flutter/packages/pull/12490) merged, and `video_player` on iOS now sends video to an AirPlay screen instead of mirroring the app. An AirPlay button inside the app still needs a platform view. Mine is one shared `AVRoutePickerView` behind a glass button.

On 17 September DartNative shipped the `PageView` this app is built on, the one I had to fake in September.

### What the timeline leaves out

Three things on this screen are missing from the timeline because Flutter never needed an issue for them. Audio with the screen locked is one flag, `VideoPlayerOptions(allowBackgroundPlayback: true)`, and it cost me native code in DartNative. Pull to refresh ships with Flutter as `RefreshIndicator` and `CupertinoSliverRefreshControl`, and does not exist in DartNative at all. The lock screen and Control Center player is the exception the other way: `video_player` has no API for it, I found no issue asking for one, and in DartNative it took my plugin as well.

### The pattern

Laid out by year, the oldest issues are about the places where Flutter draws its own version of something iOS already has: text editing, sheets, the keyboard, and now glass. DartNative skips that whole class of problem because it uses the system's own. Where DartNative falls short is the other side: features Flutter's first-party packages already ship, like background audio and pull to refresh, still needed native code from me.

## Launch went from half a second to three

Startup here is from the first line of `main()` to the first frame, three launches each, profile builds, launched without a debugger:

| Build | Startup |
|---|---|
| The September feed's plain overlays, on `PageView` | 477 to 608ms |
| The TikTok-style screen | 2,724 to 3,452ms |
| The same screen with the right-hand button column removed | 806 to 886ms |

For context only, September's feed started in 863ms on the same phone, on iOS 26.6.1 and an older SDK. That is too many changes to call anything faster.

### My own mistakes

Some of the three seconds was my fault. My first version put an AirPlay picker (`AVRoutePickerView`) and a `UIProgressView` on every page. In a Time Profiler trace of that version, creating the pickers took about half a second of main thread, because each one parses an animation package for its icon, and the progress bars took about 0.4 seconds rendering their track images. One shared picker behind a normal glass button and one progress bar over the pager fixed both, and launch was still near three seconds.

### What the trace shows

In the trace of the fixed build, Dart spent about 0.1 seconds building widgets. Almost everything else was native: creating views, and inside the Core Animation commit, Yoga laying them out. Nearly two thirds of the commit was Yoga layout, most of it spent on absolutely positioned children. In DartNative every widget is a real `UIView` with a Yoga node. A TikTok page is a stack of positioned layers with a column of glass buttons, icons and labels inside it, around 70 views, and removing that column took launch from three seconds to under one.

### PageView.builder built all 1,000 pages

Then I counted how many pages were being built. With `itemCount: 1000`, `PageView.builder` called `itemBuilder` 1,000 times before the first frame, once for every index from 0 to 999. It did it again on every `setState`, for 3,000 calls in the first 12 seconds as clips reported ready. The doc comment says a page that scrolls out of the kept window "releases what it built". In this app nothing was released because nothing was ever left unbuilt, and `allowImplicitScrolling` was already off in the run I counted. The rail was never the real problem. A thousand rails were.

I did not instrument the plain feed, so I cannot say whether it also builds a thousand pages and gets away with it because its pages are about 8 views. I also did not measure the fix I would try next, a rail with fixed sizes. If you are building on `PageView` today, put a counter in your `itemBuilder` before you trust the doc comment.

### Update: 30 pages instead of 1,000, and 10,000

After this went out, the DartNative team replied that a feed only needs about 30 pages loaded. So I ran it both ways on the same phone in one session, with nothing changed but the page count:

| Pages | Startup | First clip ready |
|---|---|---|
| 30 | 342 to 356ms | 536 to 613ms |
| 1,000 | 2,221 to 2,341ms | 2,693 to 2,850ms |
| 10,000 | never drew a frame | never |

With 30 pages the full TikTok-style screen starts faster than the plain feed did above. The builder behaves the same at every size: it built every page before the first frame and rebuilt all of them on every `setState`. At 30 that costs almost nothing. At 1,000 it is the whole launch.

At 10,000 the app never showed a frame. Dart finished building all 10,000 pages about half a second after launch, and then the native side tried to create views for all of them. The phone's memory report from those runs shows iOS killing the app at about 3.4GB, while it was still on screen. It happened on all three launches.

So in this app, `PageView.builder` is not lazy. Flutter's builds the pages near the viewport, which is why `itemCount: 10000` costs about the same as 30 there. DartNative's built every index it was given, and its cost grew with the page count until the app ran out of memory. The doc comment says pages outside the kept window are released, and that is not what I measured. Until that changes, the page count is a budget: keep it near 30.

Two things to keep in mind before copying this. A 30-page feed ends at page 30, and I have not tested a feed that grows its page count as the reader scrolls. And this build also had the fixed-size rail I mentioned above, so its 1,000-page number is not comparable with the table at the top of this section, which came from an earlier session.

## Smaller things

- The simulator build failed in DartNative's own build step with `Binary ... Flutter.framework/Flutter does not contain architectures "arm64 x86_64"`, while `lipo` on the same file lists `x86_64 arm64`. I did everything on the device instead.
- The SDK installer puts `~/zero/bin` first on your `PATH`, so `flutter` becomes DartNative's copy. The installer says so, and it will still surprise you the first time a stock Flutter project picks up the wrong toolchain.
- Current Xcode builds nothing below iOS 15. DartNative's release raised its minimum to match. My September Flutter project needed the same change before it would build at all.
- The September DartNative app, Skia and all, still built on the new SDK without edits. `dn` upgraded its project files by itself. That is the kind of thing a young framework usually gets wrong.

## A correction to the September post

In September I wrote that four `flutter run --profile` attempts hung on this phone with "The Dart VM Service was not discovered," and that I read it as a Flutter and CoreDevice tooling problem. This time `dn run --profile` hung with the same message on the same phone, and the app sat on its launch screen. DartNative's CLI is built on Flutter's tools, so one hang on each side points at the shared tooling or CoreDevice, not at Flutter. Launching the installed app with `devicectl` worked every time, and that is how every number in this post was taken.

## What I did not measure

This post is DartNative only. I rebuilt the Flutter app for this phone and it builds, but I never launched it, so there is no Flutter column and no comparison. I did not run swipe or load tests, time to first frame per clip, memory, leaks or dropped frames. Nothing above tells you whether this feed leaks as you scroll.

## What I would tell a team

If your problem is that Flutter's controls do not feel native, DartNative solves it, and this screen is my evidence. The glass, the sheet, the text field and the tab bar are the system's own, and I got them by writing Dart.

If your problem is a media app, count the native work first. Every feature that made this feel like TikTok and not a list of videos needed platform code the plugin does not expose, and my plugin reaches into internals to get it. Keep the page count small: at 30 pages this same screen started in about a third of a second. And measure launch with your real page, not the demo's. The plain feed started in half a second. Mine took three. The difference was entirely in how many native views the framework built, for pages nobody could see.

## Sources

- [video-feed-showdown](https://github.com/saileshbro/video-feed-showdown/tree/dartnative-pageview-revisit), the app, the plugin, and every log and trace behind these numbers in `bench/v2/`
- [RESULTS-v2.md](https://github.com/saileshbro/video-feed-showdown/blob/dartnative-pageview-revisit/bench/v2/RESULTS-v2.md), each figure mapped to its file
- [I built the same infinite video feed in DartNative and Flutter](/dartnative-vs-flutter-infinite-video-feed), the comparison this follows
- [DartNative wants Flutter developers to ship native UI from Dart](/dartnative-flutter-native-ui-framework), the original review
- [DartNative changelog](https://github.com/DartNative/dartnative/blob/main/CHANGELOG.md), the `PageView` release of 17 September
- [DartNative widget reference](https://github.com/DartNative/dartnative/blob/main/docs/widgets.md) and [plugin development guide](https://github.com/DartNative/dartnative/blob/main/docs/plugin_development.md)
- [DartNative's feed demo](https://github.com/DartNative/dartnative/blob/main/playground/lib/screens/media/video_feed_demo.dart)
- [Pexels API](https://www.pexels.com/api/), the source of the 10,000 clips