Skip to main content

So, You Want to Migrate to Better Player?

· 7 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 just to keep a simple video playing smoothly.

If you're currently 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 actually looks like in practice.


The Big Question: Why Better Player?

  • If you're coming from raw video_player: You're basically writing your own media player UI from scratch. Better Player gives you full player controls, fullscreen handling, subtitle support, caching, and notification hooks right out of the box.
  • If you're coming from Chewie: Chewie is a nice lightweight wrapper, but it can feel restrictive once you need deep customization, bulletproof list/feed playback, or complex HLS/DASH configurations. Better Player is built heavier, but it's faster than ever thanks to modern JNI/FFI native bridges.

Step 1: Swap the Dependencies

First things first, open up your pubspec.yaml. If you were using Chewie, kick it out.

dependencies:
flutter:
sdk: flutter
# Bye-bye, chewie! 👋
better_player: ^1.2.0

Run flutter pub get. If you're upgrading from an older version of Better Player (0.x.x), checking out the section below is your first stop.


Upgrading from Older Versions

If you're already using Better Player and just want to upgrade to the faster, cleaner modern version, there's good news: you don't have to rename everything manually.

The "Magic" Command

Flutter's dart fix tool can handle most of the renames for you. Simply run:

dart fix --apply

This will automatically swap names like BetterPlayerConfiguration to PlayerConfiguration across your entire project.

Why the Change?

This isn't just about cleaner names. It's a fundamental architectural shift:

  • Direct Native Bridges: Better Player now bypasses the slow MethodChannel serialization. It uses JNI (Android) and Swift FFI (iOS) to talk directly to the native media engines.
  • Federated Architecture: The core logic is now decoupled from platform-specific code, making it more stable and easier 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 your own 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:

// The Better Player way ✨
class MyBetterPlayer extends StatefulWidget {

_MyBetterPlayerState createState() => _MyBetterPlayerState();
}

class _MyBetterPlayerState extends State<MyBetterPlayer> {
late BetterPlayerController _betterPlayerController;


void initState() {
super.initState();

// 1. Setup your behavior and styling preferences
final 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);
}


void dispose() {
_betterPlayerController.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 16 / 9,
child: BetterPlayer(controller: _betterPlayerController),
);
}
}

That's it. It handles the loading spinner, controls overlay, and sizing out of the box.


Step 3: Migrating from Chewie

If you were already using Chewie, switching to Better Player is even less painful because the conceptual model is almost identical. Both use a controller + widget setup.

The configuration shift

In Chewie, everything lived inside the 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: Leveling Up (Advanced Features You’ll Actually Use)

Once you've got the basic player rendering, you can unlock the features that make Better Player worth the migration effort.

1. Bulletproof Video Caching

If your app has a feed, a playlist, or users who might re-watch content, caching is a lifesaver. Without it, every scroll or repeat playback re-downloads the entire stream. Better Player handles this seamlessly under the hood:

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. Subtitles That Don’t Make You Cry

Trying to overlay custom .srt or .vtt subtitles cleanly in video_player usually leads to custom UI positioning hacks. With Better Player, it's a first-class citizen:

final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/video_with_audio.mp4",
subtitles: PlayerSubtitlesSource.single(
type: PlayerSubtitlesSourceType.file,
url: "https://example.com/subtitles_en.srt",
name: "English Subtitles",
),
);

Users can toggle them right from the built-in overflow menu without you writing a single line of subtitle UI code.

3. Picture-in-Picture (PiP) Mode

Users love floating video windows, especially on mobile. Better Player hooks into native Android and iOS PiP APIs with minimal fuss. To enable it, make sure your configuration allows PiP and hook into your app lifecycle:

final config = PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: false,
allowedScreenSleep: false,
);

Step 5: Handling Feeds and Lists Like a Pro (BetterPlayerListVideoPlayer)

If you're building a scrollable feed (think TikTok, Instagram Reels, or a course video list), putting a standard BetterPlayer widget inside a ListView is a recipe for memory leaks and audio playing when videos are completely off-screen.

Instead, Better Player provides a dedicated widget called BetterPlayerListVideoPlayer for this exact scenario:

ListView.builder(
itemCount: videoUrls.length,
itemBuilder: (context, index) {
return BetterPlayerListVideoPlayer(
PlayerDataSource(
DataSourceType.network,
videoUrls[index],
),
playFraction: 0.6, // auto-play when 60% of the player is visible
autoPlay: false,
);
},
);

Behind the scenes, it automatically tracks visibility, auto-plays when the item scrolls into view, and cleanly pauses when it scrolls away. No custom intersection observers required!


Real-Talk Pitfalls to Watch Out For

Before you ship to production, keep these common gotchas in mind:

  1. The Ghost Audio Bug: Always, always call _betterPlayerController.dispose() in your widget's dispose() method. If you forget, the video player instance lingers in memory, playing audio in the background while your user is three screens away.
  2. Platform Permissions:
    • On Android, make sure your AndroidManifest.xml allows cleartext traffic if you're testing against local dev servers (android:usesCleartextTraffic="true").
    • On iOS, check your Info.plist for App Transport Security settings if your streams aren't strictly HTTPS.
  3. Over-Configuring: Better Player has a lot of knobs and switches (PlayerControlsConfiguration, translations, event listeners, custom overflows). Don't try to customize every single thing on day one. Get the basic player working stably first, then layer on the custom styling.

Wrapping Up

Migrating shouldn't feel like open heart surgery, and moving to Better Player is honestly pretty painless once you wrap your head around the Configuration + DataSource split. You'll drop a ton of custom boilerplate code and get a much more robust player experience for your users.

Have you run into any stubborn playback bugs or weird edge cases during your migration? Let us know, and we'll troubleshoot it together!


Jakub Homlala