feed_player 0.0.1
feed_player: ^0.0.1 copied to clipboard
A reusable Flutter SDK for building and playing a media feed (video-first) with clean architecture and a minimal public API.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:feed_player/feed_player.dart';
import 'comments_demo_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
FeedPlayer.initialize(
const FeedConfig(
baseUrl: 'https://api.example.com',
apiKey: 'REPLACE_ME',
feedEndpointPath: '/v1/feed',
),
);
runApp(const MyApp());
}
class DemoFeedRepository implements FeedRepository {
@override
Future<List<FeedItem>> fetchFeed({int limit = 20, String? cursor}) async {
const urls = <String>[
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
'https://user-images.githubusercontent.com/28951144/229373709-603a7a89-2105-4e1b-a5a5-a6c3567c9a59.mp4',
'https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4',
'https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4',
'https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4',
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
];
return List<FeedItem>.generate(
urls.length,
(i) => FeedItem(
id: 'demo_$i',
authorId: 'user_$i',
authorAvatarUrl: 'https://i.pravatar.cc/150?img=${i + 10}',
authorName: 'Demo Creator ${i + 1}',
metaLine: 'Just now · Demo feed',
caption:
'Swipe like TikTok. Tap heart/comment/share on the right. '
'Toggle the app bar button to switch to post cards.',
isLiked: i.isEven,
likeCount: 12 + i * 3,
commentCount: 2 + i,
shareCount: 1,
viewCount: 480 + i * 42,
media: [FeedMedia(type: FeedMediaType.video, url: urls[i])],
raw: const {'source': 'demo'},
),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Feed Player Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
),
home: const FeedHomePage(),
);
}
}
class FeedHomePage extends StatefulWidget {
const FeedHomePage({super.key});
@override
State<FeedHomePage> createState() => _FeedHomePageState();
}
class _FeedHomePageState extends State<FeedHomePage> {
bool _reelView = true;
@override
Widget build(BuildContext context) {
final feed = FeedPlayerWidget(
repository: DemoFeedRepository(),
pageSize: 20,
reelView: _reelView,
padding: const EdgeInsets.all(12),
postChrome: const FeedPostChromeOptions(
hideEngagementWithoutCallbacks: true,
),
postActions: FeedPostActions(
onProfileTap: (ctx, item) {
Navigator.of(ctx).push<void>(
MaterialPageRoute<void>(
builder: (_) => ExampleProfilePage(item: item),
),
);
},
onLikeTap: (ctx, item) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text('Like toggled for post ${item.id} — wire API'),
),
);
},
onCommentTap: (ctx, item) {
Navigator.of(ctx).push<void>(
MaterialPageRoute<void>(
builder: (_) => CommentsDemoScreen(item: item),
),
);
},
onShareTap: (ctx, item) {
ScaffoldMessenger.of(
ctx,
).showSnackBar(SnackBar(content: Text('Share post ${item.id}')));
},
onMoreTap: (ctx, item) {
showModalBottomSheet<void>(
context: ctx,
showDragHandle: true,
builder: (sheetCtx) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.flag_outlined),
title: const Text('Report'),
onTap: () => Navigator.pop(sheetCtx),
),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
title: const Text('Hide post'),
onTap: () => Navigator.pop(sheetCtx),
),
],
),
);
},
);
},
onLikesSummaryTap: (ctx, item) {
showModalBottomSheet<void>(
context: ctx,
showDragHandle: true,
builder: (sheetCtx) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: 8,
itemBuilder: (_, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('Demo liker ${index + 1}'),
subtitle: const Text(
'Replace with your likes bottom sheet + API',
),
);
},
);
},
);
},
onViewsTap: (ctx, item) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text('Views detail for ${item.id}')),
);
},
),
);
return Scaffold(
extendBodyBehindAppBar: _reelView,
appBar: AppBar(
backgroundColor: _reelView
? Colors.black.withValues(alpha: 0.35)
: Theme.of(context).colorScheme.onPrimaryFixed,
foregroundColor: _reelView ? Colors.white : null,
elevation: _reelView ? 0 : null,
title: Text(_reelView ? 'Reels (TikTok)' : 'Post feed'),
actions: [
IconButton(
tooltip: _reelView ? 'Switch to post feed' : 'Switch to reels',
icon: Icon(
_reelView
? Icons.view_agenda_outlined
: Icons.video_library_outlined,
),
onPressed: () => setState(() => _reelView = !_reelView),
),
],
),
body: _reelView ? feed : SafeArea(child: feed),
);
}
}
/// Stand-in for your profile route.
class ExampleProfilePage extends StatelessWidget {
const ExampleProfilePage({super.key, required this.item});
final FeedItem item;
@override
Widget build(BuildContext context) {
final name = item.authorName ?? item.title ?? 'Profile';
return Scaffold(
appBar: AppBar(title: Text(name)),
body: ListView(
padding: const EdgeInsets.all(24),
children: [
Center(
child: CircleAvatar(
radius: 48,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: const TextStyle(fontSize: 36),
),
),
),
const SizedBox(height: 24),
Text('authorId: ${item.authorId ?? '—'}'),
const SizedBox(height: 8),
Text('post id: ${item.id}'),
],
),
);
}
}