So, You Want to Migrate to Better Player?
Let’s be honest: we’ve all been there. You start a Flutter project, throw in the official video_player package, and think, "Hey, this is pretty easy."
Then the client (or your own product roadmap) hits you with real-world requirements:
- "Can we cache videos so users don't burn through their data?"
- "Why does the player controls overlay break when I rotate the phone?"
- "Can we add subtitles? And picture-in-picture? And a custom loading spinner?"
Suddenly, you’re drowning in platform channels, custom gesture detectors, and a spaghetti mess of state management to keep a video playing smoothly.
If you're wrapping your head around this and considering a switch to Better Player (or trying to decide between it and Chewie), let's walk through what the transition looks like in practice.
Why Better Player?
- If you're coming from raw
video_player: You're writing your own media player UI from scratch. Better Player gives you full player controls, fullscreen handling, subtitle support, caching, and notification hooks out of the box. - If you're coming from Chewie: Chewie is a lightweight wrapper, but it feels restrictive once you need deep customization, bulletproof list playback, or adaptive HLS/DASH streams. Better Player uses direct JNI (Android) and Swift FFI (iOS) bridges to native engines, giving you raw performance without the headaches.
Step 1: Swap Dependencies
Remove chewie (if you were using it) and add better_player:
flutter pub remove chewie
flutter pub add better_player
If you're upgrading from an older version of Better Player (0.x), check the upgrade section below first.
Upgrading from Older Versions
If you're already using Better Player and want to upgrade to the faster, modern 1.x series, you don't have to rename everything manually.
The "Magic" Command
Flutter's dart fix tool handles most renames for you. Run:
dart fix --apply
This automatically swaps names like BetterPlayerConfiguration to PlayerConfiguration across your entire project.
Why the Change?
This isn't about cleaner names. It's a fundamental architectural shift:
- Direct Native Bridges: Better Player bypasses slow
MethodChannelserialization. It uses JNI (Android) and Swift FFI (iOS) to talk directly to native media engines. - Federated Architecture: Core logic is decoupled from platform code, making it stable and easy to maintain.
Step 2: Ditching Raw video_player
If you've been using vanilla video_player, you know the drill: initialize the controller, add a listener, check if it's initialized, wrap it in an AspectRatio, and manually build play/pause buttons if you want anything interactive.
Here is roughly what your old code looked like:
// The old, boilerplate-heavy way
class MyOldPlayer extends StatefulWidget {
_MyOldPlayerState createState() => _MyOldPlayerState();
}
class _MyOldPlayerState extends State<MyOldPlayer> {
late VideoPlayerController _controller;
void initState() {
super.initState();
_controller = VideoPlayerController.networkUrl(Uri.parse('https://example.com/video.mp4'))
..initialize().then((_) {
setState(() {}); // wait for it...
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Center(child: CircularProgressIndicator());
}
}
Now, look at how clean that gets with Better Player. No manual initialization checks, no separate loading state boilerplate, and autoDispose: true (the default) cleans up the controller when the widget unmounts:
// The Better Player way
class MyBetterPlayer extends StatefulWidget {
_MyBetterPlayerState createState() => _MyBetterPlayerState();
}
class _MyBetterPlayerState extends State<MyBetterPlayer> {
late BetterPlayerController _betterPlayerController;
void initState() {
super.initState();
// 1. Behavior and styling preferences
const config = PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
looping: true,
);
// 2. Point it to your source
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/video.mp4",
);
_betterPlayerController = BetterPlayerController(config, betterPlayerDataSource: dataSource);
}
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 16 / 9,
child: BetterPlayer(controller: _betterPlayerController),
);
}
}
It handles the loading spinner, controls overlay, sizing, and lifecycle disposal out of the box.
Step 3: Migrating from Chewie
If you were using Chewie, switching to Better Player is quick: both use a controller and widget setup.
The configuration shift
In Chewie, everything lived inside ChewieController:
// Chewie style
final chewieController = ChewieController(
videoPlayerController: videoPlayerController,
aspectRatio: 16 / 9,
autoPlay: true,
materialProgressColors: ChewieProgressColors(playedColor: Colors.red),
);
In Better Player, configuration is cleanly split between how the player behaves (PlayerConfiguration) and where the video comes from (PlayerDataSource):
// Better Player style
final betterPlayerController = BetterPlayerController(
PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
),
betterPlayerDataSource: PlayerDataSource(
DataSourceType.network,
"https://example.com/video.mp4",
),
);
Step 4: Features You’ll Actually Use
Once you have the basic player rendering, you can unlock features that make Better Player worth the migration.
1. Reliable Video Caching
If your app has a feed, a playlist, or users who re-watch content, caching is essential. Without it, every repeat playback re-downloads the entire stream. Better Player caches chunks to disk automatically:
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/heavy_video.mp4",
cacheConfiguration: CacheConfiguration(
useCache: true,
maxCacheSize: 200 * 1024 * 1024, // 200 MB max cache pool
maxCacheFileSize: 20 * 1024 * 1024, // individual file ceiling
),
);
2. Clean Subtitles
Overlaying custom .srt or .vtt subtitles in video_player usually leads to fragile UI positioning hacks. With Better Player, subtitles are built-in:
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/video_with_audio.mp4",
subtitles: [
PlayerSubtitlesSource(
type: PlayerSubtitlesSourceType.network,
name: "English Subtitles",
urls: ["https://example.com/subtitles_en.srt"],
),
],
);
Users can toggle them from the built-in overflow menu without custom UI code.
3. Picture-in-Picture (PiP) Mode
Floating video windows work on Android and iOS with native PiP APIs. Enable it in your configuration:
final config = PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: false,
allowedScreenSleep: false,
);
Step 5: Pooled Feeds (BetterPlayerFeed & BetterPlayerPool)
If you're building a vertical swipe feed (TikTok, Reels, or course video feeds), putting unpooled players inside a scrolling list exhausts hardware decoders and spikes memory.
Better Player provides BetterPlayerFeed backed by a bounded BetterPlayerPool:
final feedController = BetterPlayerFeedController(
dataSources: videoUrls.map((url) {
return PlayerDataSource(DataSourceType.network, url);
}).toList(),
poolConfiguration: const PlayerPoolConfiguration(
poolSize: 3,
preloadForward: 1,
preloadBackward: 1,
),
);
BetterPlayerFeed(
controller: feedController,
configuration: PlayerFeedConfiguration(
scrollDirection: Axis.vertical,
placeholderBuilder: (context, index, dataSource) =>
const Center(child: CircularProgressIndicator()),
),
);
It recycles a fixed pool of native decoder instances, preloads neighboring items in the background, and releases leases automatically as the user swipes.
Pitfalls to Watch Out For
Before you ship to production, keep these gotchas in mind:
- Don't Double-Dispose:
PlayerConfiguration.autoDisposedefaults totrue, soBetterPlayerautomatically disposes itsBetterPlayerControllerwhen the widget unmounts. Only call_betterPlayerController.dispose()manually inState.dispose()if you setautoDispose: false. - Platform Permissions:
- On Android, set
android:usesCleartextTraffic="true"in yourAndroidManifest.xmlif you test against local HTTP servers. - On iOS, check your
Info.plistfor App Transport Security settings if your streams aren't strictly HTTPS.
- On Android, set
- Over-Configuring: Better Player has lots of knobs and switches (
PlayerControlsConfiguration, translations, event listeners, custom overflows). Get the basic player working first, then layer on custom styling.
Wrapping Up
Migrating shouldn't feel like open heart surgery. Moving to Better Player is painless once you understand the Configuration and DataSource split. You drop a pile of boilerplate and give your users a rock-solid media player.
Have you run into stubborn playback bugs or weird edge cases during your migration? Let us know, and we'll troubleshoot it together.
