diff --git a/demos/django-todolist/lib/main.dart b/demos/django-todolist/lib/main.dart index 5fad8f77..f053b806 100644 --- a/demos/django-todolist/lib/main.dart +++ b/demos/django-todolist/lib/main.dart @@ -41,7 +41,7 @@ const listsPage = ListsPage(); const homePage = listsPage; const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), + appBar: StatusAppBar(title: Text('SQL Console')), body: QueryWidget(defaultQuery: defaultQuery)); const loginPage = LoginPage(); @@ -76,7 +76,7 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: StatusAppBar(title: title), + appBar: StatusAppBar(title: Text(title)), body: Center(child: content), floatingActionButton: floatingActionButton, drawer: Drawer( diff --git a/demos/django-todolist/lib/widgets/lists_page.dart b/demos/django-todolist/lib/widgets/lists_page.dart index e31c2fc8..aba21fb2 100644 --- a/demos/django-todolist/lib/widgets/lists_page.dart +++ b/demos/django-todolist/lib/widgets/lists_page.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import '../powersync.dart'; import './list_item.dart'; import './list_item_dialog.dart'; import '../main.dart'; @@ -41,48 +40,32 @@ class ListsPage extends StatelessWidget { } } -class ListsWidget extends StatefulWidget { +class ListsWidget extends StatelessWidget { const ListsWidget({super.key}); - @override - State createState() { - return _ListsWidgetState(); - } -} - -class _ListsWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - _ListsWidgetState(); - - @override - void initState() { - super.initState(); - final stream = TodoList.watchListsWithStats(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), + return FutureBuilder( + future: db.waitForFirstSync(), + builder: (context, snapshot) { + return switch (snapshot.connectionState) { + ConnectionState.done => StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + }, + ), + // waitForFirstSync() did not complete yet + _ => const Text('Busy with sync...'), + }; + }, ); } } diff --git a/demos/django-todolist/lib/widgets/status_app_bar.dart b/demos/django-todolist/lib/widgets/status_app_bar.dart index 90d18ae8..9011303e 100644 --- a/demos/django-todolist/lib/widgets/status_app_bar.dart +++ b/demos/django-todolist/lib/widgets/status_app_bar.dart @@ -1,62 +1,42 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import 'package:powersync_django_todolist_demo/widgets/fts_search_delegate.dart'; import '../powersync.dart'; -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - - final String title; +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; - @override - State createState() => _StatusAppBarState(); + const StatusAppBar({super.key, required this.title}); @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } @override Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState); + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); - return AppBar( - title: Text(widget.title), - actions: [ - IconButton( - onPressed: () { - showSearch(context: context, delegate: FtsSearchDelegate()); - }, - icon: const Icon(Icons.search), - ), - statusIcon, - // Make some space for the "Debug" banner, so that the status - // icon isn't hidden - if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), - ], + return AppBar( + title: title, + actions: [ + IconButton( + onPressed: () { + showSearch(context: context, delegate: FtsSearchDelegate()); + }, + icon: const Icon(Icons.search), + ), + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, ); } } diff --git a/demos/django-todolist/lib/widgets/todo_list_page.dart b/demos/django-todolist/lib/widgets/todo_list_page.dart index 26dbae63..a457eb98 100644 --- a/demos/django-todolist/lib/widgets/todo_list_page.dart +++ b/demos/django-todolist/lib/widgets/todo_list_page.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:powersync_django_todolist_demo/models/todo_item.dart'; import './status_app_bar.dart'; import './todo_item_dialog.dart'; @@ -34,56 +31,31 @@ class TodoListPage extends StatelessWidget { ); return Scaffold( - appBar: StatusAppBar(title: list.name), + appBar: StatusAppBar(title: Text(list.name)), floatingActionButton: button, body: TodoListWidget(list: list)); } } -class TodoListWidget extends StatefulWidget { +class TodoListWidget extends StatelessWidget { final TodoList list; const TodoListWidget({super.key, required this.list}); - @override - State createState() { - return TodoListWidgetState(); - } -} - -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - TodoListWidgetState(); - - @override - void initState() { - super.initState(); - final stream = widget.list.watchItems(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, ); } } diff --git a/demos/firebase-nodejs-todolist/lib/main.dart b/demos/firebase-nodejs-todolist/lib/main.dart index b5143b5d..8a4bc617 100644 --- a/demos/firebase-nodejs-todolist/lib/main.dart +++ b/demos/firebase-nodejs-todolist/lib/main.dart @@ -40,7 +40,7 @@ const listsPage = ListsPage(); const homePage = listsPage; const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), + appBar: StatusAppBar(title: Text('SQL Console')), body: QueryWidget(defaultQuery: defaultQuery)); const loginPage = LoginPage(); @@ -77,7 +77,7 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: StatusAppBar(title: title), + appBar: StatusAppBar(title: Text(title)), body: Center(child: content), floatingActionButton: floatingActionButton, drawer: Drawer( diff --git a/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart index 933fabc1..aba21fb2 100644 --- a/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import '../powersync.dart'; import './list_item.dart'; import './list_item_dialog.dart'; import '../main.dart'; @@ -41,48 +40,32 @@ class ListsPage extends StatelessWidget { } } -class ListsWidget extends StatefulWidget { +class ListsWidget extends StatelessWidget { const ListsWidget({super.key}); - @override - State createState() { - return _ListsWidgetState(); - } -} - -class _ListsWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - _ListsWidgetState(); - - @override - void initState() { - super.initState(); - final stream = TodoList.watchListsWithStats(); - _subscription = stream.listen((data) { - if (!mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), + return FutureBuilder( + future: db.waitForFirstSync(), + builder: (context, snapshot) { + return switch (snapshot.connectionState) { + ConnectionState.done => StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + }, + ), + // waitForFirstSync() did not complete yet + _ => const Text('Busy with sync...'), + }; + }, ); } } diff --git a/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart b/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart index 257a9996..19c1a025 100644 --- a/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart @@ -1,95 +1,67 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import '../powersync.dart'; -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - - final String title; +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; - @override - State createState() => _StatusAppBarState(); + const StatusAppBar({super.key, required this.title}); @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } @override Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState, context); + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); - return AppBar( - title: Text(widget.title), - actions: [ - statusIcon, - // Make some space for the "Debug" banner, so that the status - // icon isn't hidden - if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode, context), - ], + return AppBar( + title: title, + actions: [ + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, ); } } -Widget _makeIcon(String text, IconData icon, BuildContext context) { +Widget _makeIcon(String text, IconData icon) { return Tooltip( message: text, - child: InkWell( - onTap: () { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Status: $text'))); - }, - child: - SizedBox(width: 40, height: null, child: Icon(icon, size: 24)))); + child: SizedBox(width: 40, height: null, child: Icon(icon, size: 24))); } -Widget _getStatusIcon(SyncStatus status, BuildContext context) { +Widget _getStatusIcon(SyncStatus status) { if (status.anyError != null) { // The error message is verbose, could be replaced with something // more user-friendly if (!status.connected) { - return _makeIcon(status.anyError!.toString(), Icons.cloud_off, context); + return _makeIcon(status.anyError!.toString(), Icons.cloud_off); } else { - return _makeIcon( - status.anyError!.toString(), Icons.sync_problem, context); + return _makeIcon(status.anyError!.toString(), Icons.sync_problem); } } else if (status.connecting) { - return _makeIcon('Connecting', Icons.cloud_sync_outlined, context); + return _makeIcon('Connecting', Icons.cloud_sync_outlined); } else if (!status.connected) { - return _makeIcon('Not connected', Icons.cloud_off, context); + return _makeIcon('Not connected', Icons.cloud_off); } else if (status.uploading && status.downloading) { // The status changes often between downloading, uploading and both, // so we use the same icon for all three - return _makeIcon( - 'Uploading and downloading', Icons.cloud_sync_outlined, context); + return _makeIcon('Uploading and downloading', Icons.cloud_sync_outlined); } else if (status.uploading) { - return _makeIcon('Uploading', Icons.cloud_sync_outlined, context); + return _makeIcon('Uploading', Icons.cloud_sync_outlined); } else if (status.downloading) { - return _makeIcon('Downloading', Icons.cloud_sync_outlined, context); + return _makeIcon('Downloading', Icons.cloud_sync_outlined); } else { - return _makeIcon('Connected', Icons.cloud_queue, context); + return _makeIcon('Connected', Icons.cloud_queue); } } diff --git a/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart index 5ec8bbdd..a457eb98 100644 --- a/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:firebase_nodejs_todolist/models/todo_item.dart'; import './status_app_bar.dart'; import './todo_item_dialog.dart'; @@ -34,56 +31,31 @@ class TodoListPage extends StatelessWidget { ); return Scaffold( - appBar: StatusAppBar(title: list.name), + appBar: StatusAppBar(title: Text(list.name)), floatingActionButton: button, body: TodoListWidget(list: list)); } } -class TodoListWidget extends StatefulWidget { +class TodoListWidget extends StatelessWidget { final TodoList list; const TodoListWidget({super.key, required this.list}); - @override - State createState() { - return TodoListWidgetState(); - } -} - -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - TodoListWidgetState(); - - @override - void initState() { - super.initState(); - final stream = widget.list.watchItems(); - _subscription = stream.listen((data) { - if (!mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, ); } } diff --git a/demos/supabase-todolist/lib/main.dart b/demos/supabase-todolist/lib/main.dart index f45f2b21..7f9dac55 100644 --- a/demos/supabase-todolist/lib/main.dart +++ b/demos/supabase-todolist/lib/main.dart @@ -46,7 +46,7 @@ const listsPage = ListsPage(); const homePage = listsPage; const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), + appBar: StatusAppBar(title: Text('SQL Console')), body: QueryWidget(defaultQuery: defaultQuery)); const loginPage = LoginPage(); @@ -83,7 +83,7 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: StatusAppBar(title: title), + appBar: StatusAppBar(title: Text(title)), body: Center(child: content), floatingActionButton: floatingActionButton, drawer: Drawer( diff --git a/demos/supabase-todolist/lib/widgets/lists_page.dart b/demos/supabase-todolist/lib/widgets/lists_page.dart index d60cb9f5..431a95de 100644 --- a/demos/supabase-todolist/lib/widgets/lists_page.dart +++ b/demos/supabase-todolist/lib/widgets/lists_page.dart @@ -41,7 +41,7 @@ class ListsPage extends StatelessWidget { } } -final class ListsWidget extends StatelessWidget { +class ListsWidget extends StatelessWidget { const ListsWidget({super.key}); @override @@ -49,25 +49,23 @@ final class ListsWidget extends StatelessWidget { return FutureBuilder( future: db.waitForFirstSync(priority: _listsPriority), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return StreamBuilder( - stream: TodoList.watchListsWithStats(), - builder: (context, snapshot) { - if (snapshot.data case final todoLists?) { + return switch (snapshot.connectionState) { + ConnectionState.done => StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + return ListView( padding: const EdgeInsets.symmetric(vertical: 8.0), - children: todoLists.map((list) { + children: items.map((list) { return ListItemWidget(list: list); }).toList(), ); - } else { - return const CircularProgressIndicator(); - } - }, - ); - } else { - return const Text('Busy with sync...'); - } + }, + ), + // waitForFirstSync() did not complete yet + _ => const Text('Busy with sync...'), + }; }, ); } diff --git a/demos/supabase-todolist/lib/widgets/status_app_bar.dart b/demos/supabase-todolist/lib/widgets/status_app_bar.dart index 5722cc88..c3523d83 100644 --- a/demos/supabase-todolist/lib/widgets/status_app_bar.dart +++ b/demos/supabase-todolist/lib/widgets/status_app_bar.dart @@ -1,62 +1,42 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import 'package:powersync_flutter_demo/widgets/fts_search_delegate.dart'; import '../powersync.dart'; -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - - final String title; +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; - @override - State createState() => _StatusAppBarState(); + const StatusAppBar({super.key, required this.title}); @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } @override Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState); + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); - return AppBar( - title: Text(widget.title), - actions: [ - IconButton( - onPressed: () { - showSearch(context: context, delegate: FtsSearchDelegate()); - }, - icon: const Icon(Icons.search), - ), - statusIcon, - // Make some space for the "Debug" banner, so that the status - // icon isn't hidden - if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), - ], + return AppBar( + title: title, + actions: [ + IconButton( + onPressed: () { + showSearch(context: context, delegate: FtsSearchDelegate()); + }, + icon: const Icon(Icons.search), + ), + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, ); } } diff --git a/demos/supabase-todolist/lib/widgets/todo_list_page.dart b/demos/supabase-todolist/lib/widgets/todo_list_page.dart index 57f1d561..7e28238e 100644 --- a/demos/supabase-todolist/lib/widgets/todo_list_page.dart +++ b/demos/supabase-todolist/lib/widgets/todo_list_page.dart @@ -1,9 +1,6 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:powersync_flutter_demo/models/todo_item.dart'; -import 'package:powersync_flutter_demo/powersync.dart'; +import '../powersync.dart'; import './status_app_bar.dart'; import './todo_item_dialog.dart'; import './todo_item_widget.dart'; @@ -35,64 +32,35 @@ class TodoListPage extends StatelessWidget { ); return Scaffold( - appBar: StatusAppBar(title: list.name), + appBar: StatusAppBar(title: Text(list.name)), floatingActionButton: button, body: TodoListWidget(list: list)); } } -class TodoListWidget extends StatefulWidget { +class TodoListWidget extends StatelessWidget { final TodoList list; const TodoListWidget({super.key, required this.list}); - @override - State createState() { - return TodoListWidgetState(); - } -} - -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - TodoListWidgetState(); - - @override - void initState() { - super.initState(); - final stream = widget.list.watchItems(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { return StreamBuilder( stream: TodoList.watchSyncStatus().map((e) => e.hasSynced), initialData: db.currentStatus.hasSynced, builder: (context, snapshot) { - if (snapshot.data != true) { - return const Text('Busy with sync'); - } - - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, ); }, );