Grain flutter app
1import 'package:flutter/foundation.dart';
2import 'package:flutter/material.dart';
3import 'package:flutter_dotenv/flutter_dotenv.dart';
4import 'package:flutter_riverpod/flutter_riverpod.dart';
5import 'package:grain/api.dart';
6import 'package:grain/app_logger.dart';
7import 'package:grain/app_theme.dart';
8import 'package:grain/auth.dart';
9import 'package:grain/providers/notifications_provider.dart';
10import 'package:grain/providers/profile_provider.dart';
11import 'package:grain/screens/home_page.dart';
12import 'package:grain/screens/login_page.dart';
13import 'package:grain/websocket_service.dart';
14
15import 'widgets/skeleton_timeline.dart';
16
17class AppConfig {
18 static late final String apiUrl;
19 static late final String wsUrl;
20
21 static Future<void> init() async {
22 if (!kReleaseMode) {
23 await dotenv.load(fileName: '.env');
24 }
25 apiUrl = kReleaseMode
26 ? const String.fromEnvironment('API_URL', defaultValue: 'https://grain.social')
27 : dotenv.env['API_URL'] ?? '';
28 wsUrl = kReleaseMode
29 ? const String.fromEnvironment(
30 'WS_URL',
31 defaultValue: 'wss://notifications.grainsocial.network/ws',
32 )
33 : dotenv.env['WS_URL'] ?? '';
34 }
35}
36
37Future<void> main() async {
38 WidgetsFlutterBinding.ensureInitialized();
39 FlutterError.onError = (FlutterErrorDetails details) {
40 FlutterError.presentError(details);
41 appLogger.e('Flutter error: ${details.exception}\n${details.stack}');
42 };
43 await AppConfig.init();
44 appLogger.i('🚀 App started');
45 runApp(const ProviderScope(child: MyApp()));
46}
47
48class MyApp extends StatefulWidget {
49 const MyApp({super.key});
50
51 @override
52 State<MyApp> createState() => _MyAppState();
53}
54
55class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
56 bool isSignedIn = false;
57 bool _loading = true;
58 WebSocketService? _wsService;
59
60 @override
61 void initState() {
62 super.initState();
63 WidgetsBinding.instance.addObserver(this);
64 _checkToken();
65 }
66
67 @override
68 void dispose() {
69 WidgetsBinding.instance.removeObserver(this);
70 _disconnectWebSocket();
71 super.dispose();
72 }
73
74 Future<void> _connectWebSocket() async {
75 if (_wsService != null) return; // Already connected
76 _disconnectWebSocket();
77 if (!isSignedIn) return;
78 final session = await auth.getValidSession();
79 if (session == null) return;
80 _wsService = WebSocketService(
81 wsUrl: AppConfig.wsUrl,
82 accessToken: session.token,
83 onMessage: (message) {
84 // Optionally: handle global messages or trigger provider updates
85 },
86 );
87 _wsService!.connect();
88 }
89
90 void _disconnectWebSocket() {
91 _wsService?.disconnect();
92 _wsService = null;
93 }
94
95 @override
96 void didChangeAppLifecycleState(AppLifecycleState state) {
97 if (state == AppLifecycleState.resumed && isSignedIn) {
98 // ignore: unawaited_futures
99 _connectWebSocket();
100 } else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
101 _disconnectWebSocket();
102 }
103 }
104
105 Future<void> _checkToken() async {
106 final user = await apiService.fetchCurrentUser();
107 final valid = user != null;
108 setState(() {
109 isSignedIn = valid;
110 _loading = false;
111 });
112 }
113
114 // Invalidate providers to refresh data
115 void _invalidateProviders() {
116 final container = ProviderScope.containerOf(context, listen: false);
117 container.invalidate(profileNotifierProvider);
118 container.invalidate(notificationsProvider);
119 }
120
121 void _handleSignIn() async {
122 setState(() {
123 isSignedIn = true;
124 });
125 appLogger.i('Fetching current user after sign in');
126 await apiService.fetchCurrentUser();
127 await _connectWebSocket();
128 _invalidateProviders();
129 }
130
131 void _handleSignOut(BuildContext context) async {
132 await auth.logout();
133 setState(() {
134 isSignedIn = false;
135 });
136 _disconnectWebSocket();
137 }
138
139 @override
140 Widget build(BuildContext context) {
141 Widget home;
142 if (_loading) {
143 home = Scaffold(
144 appBar: AppBar(title: const Text('Grain')),
145 body: Column(
146 children: [
147 Expanded(
148 child: SkeletonTimeline(padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8)),
149 ),
150 ],
151 ),
152 );
153 } else {
154 home = isSignedIn
155 ? MyHomePage(title: 'Grain', onSignOut: () => _handleSignOut(context))
156 : LoginPage(onSignIn: _handleSignIn);
157 }
158 return MaterialApp(
159 title: 'Grain',
160 theme: AppTheme.lightTheme,
161 darkTheme: AppTheme.darkTheme,
162 themeMode: ThemeMode.system,
163 home: home,
164 );
165 }
166}