insightreader_sdk 1.0.0
insightreader_sdk: ^1.0.0 copied to clipboard
Personalisation for Flutter news apps — reading tracking, time-of-day category insights, streaks, curated notifications, personalised feeds and Gemini AI summaries.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:insightreader_sdk/insightreader_sdk.dart';
import 'sample_data.dart';
import 'screens/ai_screen.dart';
import 'screens/article_screen.dart';
import 'screens/feed_screen.dart';
import 'screens/insights_screen.dart';
// Demo harness for the Insightreader SDK. Tracking, categories, streaks,
// notifications and feeds all work without a Gemini key; only the AI tab needs
// one. See `kGeminiApiKey` below for how to supply it.
/// The Gemini API key handed to the SDK at initialization.
///
/// Supply it one of two ways — you do not need both:
///
/// 1. **Build-time define (recommended).** Leave the fallback below empty and
/// pass the key on the command line, so it never enters source control:
///
/// ```bash
/// flutter run --dart-define-from-file=env.json
/// ```
///
/// 2. **Paste it here.** Replace the empty fallback string with your key. This
/// is the quickest way to try the AI tab, but the key then lives in source —
/// fine for a throwaway local test, never for anything you commit or ship.
const String kGeminiApiKey = String.fromEnvironment(
'GEMINI_API_KEY',
defaultValue: '', // ← or paste your key here for a quick local test
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await InsightreaderSdk.instance.initialize(
configuration: InsightreaderConfiguration(
// The key reaches the SDK here, at initialization — the SDK never
// reads it from the environment itself, so a host app is free to
// fetch it from its own backend or remote config instead.
gemini: const GeminiConfiguration(apiKey: kGeminiApiKey),
// The demo runs without Firebase, so the shared AI cache is off. A
// production host that wants it calls Firebase.initializeApp() first
// and drops this line.
aiCache: AiCacheConfiguration.disabled,
onStreakMilestone: (days) =>
AppMessenger.show('🔥 $days-day reading streak!'),
),
);
} on InsightreaderException catch (error) {
// A host that is not on the client allowlist still starts; the SDK's
// widgets simply render as empty boxes.
debugPrint('Insightreader unavailable: ${error.message}');
}
// Forward the SDK's telemetry wherever the host already sends analytics.
InsightreaderSdk.instance.analyticsEventHandler = (name, parameters) =>
debugPrint('[analytics] $name $parameters');
runApp(const ExampleApp());
}
/// Lets any screen surface a snack bar without threading a context through.
abstract final class AppMessenger {
static final GlobalKey<ScaffoldMessengerState> key =
GlobalKey<ScaffoldMessengerState>();
static void show(String message) => key.currentState
?..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Insightreader Demo',
scaffoldMessengerKey: AppMessenger.key,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF3E66DF),
useMaterial3: true,
),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF3E66DF),
brightness: Brightness.dark,
useMaterial3: true,
),
home: const HomeShell(),
);
}
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _tab = 0;
@override
void initState() {
super.initState();
_wireNotifications();
}
Future<void> _wireNotifications() async {
if (!InsightreaderSdk.instance.isInitialized) return;
// The SDK never navigates on its own — the host routes every tap.
InsightreaderSdk.instance.onNotificationTap.listen((tap) {
AppMessenger.show('Notification tapped: ${tap.type.wireName}');
if (tap.type == InsightreaderNotificationType.briefing) {
setState(() => _tab = 0);
} else {
setState(() => _tab = 2);
}
});
final launch = InsightreaderSdk.instance.launchNotification;
if (launch != null) {
AppMessenger.show('Launched from ${launch.type.wireName}');
}
}
@override
Widget build(BuildContext context) => Scaffold(
body: IndexedStack(
index: _tab,
children: const [
ArticleListScreen(),
FeedScreen(),
InsightsScreen(),
AiScreen(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _tab,
onDestinationSelected: (index) => setState(() => _tab = index),
destinations: const [
NavigationDestination(
icon: Icon(Icons.article_outlined),
selectedIcon: Icon(Icons.article),
label: 'Read',
),
NavigationDestination(
icon: Icon(Icons.dynamic_feed_outlined),
selectedIcon: Icon(Icons.dynamic_feed),
label: 'Feed',
),
NavigationDestination(
icon: Icon(Icons.insights_outlined),
selectedIcon: Icon(Icons.insights),
label: 'Insights',
),
NavigationDestination(
icon: Icon(Icons.auto_awesome_outlined),
selectedIcon: Icon(Icons.auto_awesome),
label: 'AI',
),
],
),
);
}
/// Tab 1 — a list of sample stories. Opening one records a read, which is what
/// drives every other tab.
class ArticleListScreen extends StatelessWidget {
const ArticleListScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Read something'),
actions: [
IconButton(
tooltip: 'Reading preferences',
icon: const Icon(Icons.settings_outlined),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const InsightreaderSettingsScreen(),
),
),
),
],
),
body: ListView.separated(
itemCount: sampleArticles.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final article = sampleArticles[index];
return ListTile(
title: Text(article.title),
subtitle: Text(article.category),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ArticleScreen(article: article),
),
),
);
},
),
);
}