Skip to main content

So, You Want to Migrate to Better Player?

· 6 min read
Jakub Homlala
Senior Mobile Engineer

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 MethodChannel serialization. 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:

  1. Don't Double-Dispose: PlayerConfiguration.autoDispose defaults to true, so BetterPlayer automatically disposes its BetterPlayerController when the widget unmounts. Only call _betterPlayerController.dispose() manually in State.dispose() if you set autoDispose: false.
  2. Platform Permissions:
    • On Android, set android:usesCleartextTraffic="true" in your AndroidManifest.xml if you test against local HTTP servers.
    • On iOS, check your Info.plist for App Transport Security settings if your streams aren't strictly HTTPS.
  3. 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.