Skip to content

Commit 5af49d3

Browse files
committed
refactor: replace core client singleton by provider
This is a preparation for introducing a custom router and splitting the core client into separate feature-specific classes. The `CoreClient` instance can now be access from `context` via a provider with `context.read<CoreClient>`. There is also a shortcut available via an extension of `BuildContext`: `context.coreClient`. Also: * Move `App` into a separate file * Configure Flutter logging in main * Init Flutter Rust Bridge in main * Move out theme data into a separate file
1 parent f5b685a commit 5af49d3

26 files changed

+295
-188
lines changed

prototype/devtools_options.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-FileCopyrightText: 2024 Phoenix R&D GmbH <[email protected]>
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
description: This file stores settings for Dart & Flutter DevTools.
6+
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
7+
extensions:

prototype/integration_test/simple_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
// SPDX-License-Identifier: AGPL-3.0-or-later
44

55
import 'package:flutter_test/flutter_test.dart';
6-
import 'package:prototype/main.dart';
6+
import 'package:prototype/app.dart';
77
import 'package:prototype/core/frb_generated.dart';
88
import 'package:integration_test/integration_test.dart';
99

1010
void main() {
1111
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1212
setUpAll(() async => await RustLib.init());
1313
testWidgets('Can call rust function', (WidgetTester tester) async {
14-
await tester.pumpWidget(const MyApp());
14+
await tester.pumpWidget(const App());
1515
expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget);
1616
});
1717
}

prototype/lib/app.dart

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-FileCopyrightText: 2024 Phoenix R&D GmbH <[email protected]>
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
import 'dart:io';
6+
7+
import 'package:flutter/material.dart';
8+
import 'package:logging/logging.dart';
9+
import 'package:permission_handler/permission_handler.dart';
10+
import 'package:prototype/core_client.dart';
11+
import 'package:prototype/homescreen.dart';
12+
import 'package:prototype/platform.dart';
13+
import 'package:provider/provider.dart';
14+
15+
import 'theme/theme.dart';
16+
17+
final GlobalKey<NavigatorState> appNavigator = GlobalKey<NavigatorState>();
18+
19+
final _log = Logger('App');
20+
21+
class App extends StatefulWidget {
22+
const App({super.key});
23+
24+
@override
25+
State<App> createState() => _AppState();
26+
}
27+
28+
class _AppState extends State<App> with WidgetsBindingObserver {
29+
final CoreClient _coreClient = CoreClient();
30+
31+
@override
32+
void initState() {
33+
super.initState();
34+
WidgetsBinding.instance.addObserver(this);
35+
_requestMobileNotifications();
36+
}
37+
38+
@override
39+
void dispose() {
40+
WidgetsBinding.instance.removeObserver(this);
41+
super.dispose();
42+
}
43+
44+
@override
45+
void didChangeAppLifecycleState(AppLifecycleState state) {
46+
super.didChangeAppLifecycleState(state);
47+
_onStateChanged(state);
48+
}
49+
50+
Future<void> _onStateChanged(AppLifecycleState state) async {
51+
if (state == AppLifecycleState.paused) {
52+
_log.fine('App is in the background');
53+
54+
// iOS only
55+
if (Platform.isIOS) {
56+
// only set the badge count if the user is logged in
57+
if (_coreClient.maybeUser case final user?) {
58+
final count = await user.globalUnreadMessagesCount();
59+
await setBadgeCount(count);
60+
}
61+
}
62+
}
63+
}
64+
65+
@override
66+
Widget build(BuildContext context) {
67+
// TODO: This provider should be moved below the `MaterialApp`. This can be
68+
// done when the app router is introduced. We can't just wrap the
69+
// `HomeScreen` because it is replaced in other places by another screens.
70+
return Provider.value(
71+
value: _coreClient,
72+
child: MaterialApp(
73+
title: 'Prototype',
74+
debugShowCheckedModeBanner: false,
75+
theme: themeData(context),
76+
navigatorKey: appNavigator,
77+
home: const HomeScreen(),
78+
),
79+
);
80+
}
81+
}
82+
83+
void _requestMobileNotifications() async {
84+
// Mobile initialization
85+
if (Platform.isAndroid || Platform.isIOS) {
86+
// Initialize the method channel
87+
initMethodChannel();
88+
89+
// Ask for notification permission
90+
var status = await Permission.notification.status;
91+
switch (status) {
92+
case PermissionStatus.denied:
93+
_log.info("Notification permission denied, will ask the user");
94+
var requestStatus = await Permission.notification.request();
95+
_log.fine("The status is $requestStatus");
96+
break;
97+
default:
98+
_log.info("Notification permission status: $status");
99+
}
100+
}
101+
}

prototype/lib/conversation_list_pane/conversation_list.dart

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import 'package:prototype/core_client.dart';
1111
import 'package:prototype/conversation_pane/conversation_pane.dart';
1212
import 'package:prototype/elements.dart';
1313
import 'package:prototype/messenger_view.dart';
14+
import 'package:prototype/styles.dart';
1415
import 'package:prototype/theme/theme.dart';
15-
import '../styles.dart';
1616
import 'package:convert/convert.dart';
1717
import 'package:collection/collection.dart';
1818

@@ -24,6 +24,8 @@ class ConversationList extends StatefulWidget {
2424
}
2525

2626
class _ConversationListState extends State<ConversationList> {
27+
_ConversationListState();
28+
2729
late List<UiConversationDetails> _conversations;
2830
UiConversationDetails? _currentConversation;
2931
StreamSubscription<ConversationIdBytes>? _conversationListUpdateListener;
@@ -32,19 +34,19 @@ class _ConversationListState extends State<ConversationList> {
3234

3335
static const double _topBaseline = 12;
3436

35-
_ConversationListState() {
37+
@override
38+
void initState() {
39+
super.initState();
40+
41+
final coreClient = context.coreClient;
3642
_conversations = coreClient.conversationsList;
3743
_currentConversation = coreClient.currentConversation;
3844
_conversationListUpdateListener = coreClient.onConversationListUpdate
3945
.listen(conversationListUpdateListener);
4046
_conversationSwitchListener =
4147
coreClient.onConversationSwitch.listen(conversationSwitchListener);
42-
}
4348

44-
@override
45-
void initState() {
46-
super.initState();
47-
updateConversationList();
49+
updateConversationList(coreClient);
4850
}
4951

5052
@override
@@ -66,7 +68,10 @@ class _ConversationListState extends State<ConversationList> {
6668
}
6769
}
6870

69-
void selectConversation(ConversationIdBytes conversationId) {
71+
void selectConversation(
72+
CoreClient coreClient,
73+
ConversationIdBytes conversationId,
74+
) {
7075
print("Tapped on conversation ${hex.encode(conversationId.bytes)}");
7176
coreClient.selectConversation(conversationId);
7277
if (isSmallScreen(context)) {
@@ -75,10 +80,10 @@ class _ConversationListState extends State<ConversationList> {
7580
}
7681

7782
void conversationListUpdateListener(ConversationIdBytes uuid) async {
78-
updateConversationList();
83+
updateConversationList(context.coreClient);
7984
}
8085

81-
void updateConversationList() async {
86+
void updateConversationList(CoreClient coreClient) async {
8287
await coreClient.conversations().then((conversations) {
8388
setState(() {
8489
if (_currentConversation == null && conversations.isNotEmpty) {
@@ -147,6 +152,8 @@ class _ConversationListState extends State<ConversationList> {
147152

148153
final senderStyle = style.copyWith(fontVariations: variationSemiBold);
149154

155+
final coreClient = context.coreClient;
156+
150157
if (lastMessage != null) {
151158
lastMessage.message.when(
152159
contentFlight: (c) {
@@ -290,7 +297,10 @@ class _ConversationListState extends State<ConversationList> {
290297
selected: isConversationSelected(
291298
_currentConversation, _conversations[index], context),
292299
focusColor: convListItemSelectedColor,
293-
onTap: () => selectConversation(_conversations[index].id),
300+
onTap: () => selectConversation(
301+
context.coreClient,
302+
_conversations[index].id,
303+
),
294304
);
295305
}
296306

prototype/lib/conversation_list_pane/footer.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ConversationListFooter extends StatelessWidget {
1616

1717
@override
1818
Widget build(BuildContext context) {
19+
final coreClient = context.coreClient;
1920
return Container(
2021
alignment: AlignmentDirectional.topStart,
2122
padding: const EdgeInsets.fromLTRB(15, 15, 15, 30),
@@ -43,8 +44,10 @@ class ConversationListFooter extends StatelessWidget {
4344
try {
4445
await coreClient.createConnection(connectionUsername);
4546
} catch (e) {
46-
showErrorBanner(context,
47-
'The user $connectionUsername could not be found');
47+
if (context.mounted) {
48+
showErrorBanner(context,
49+
'The user $connectionUsername could not be found');
50+
}
4851
}
4952
}
5053
},

prototype/lib/conversation_list_pane/pane.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ class ConversationView extends StatefulWidget {
2020
}
2121

2222
class _ConversationViewState extends State<ConversationView> {
23-
String? displayName = coreClient.ownProfile.displayName;
24-
Uint8List? profilePicture = coreClient.ownProfile.profilePictureOption;
23+
String? displayName;
24+
Uint8List? profilePicture;
2525

2626
@override
2727
void initState() {
2828
super.initState();
29+
30+
final coreClient = context.coreClient;
31+
setState(() {
32+
displayName = coreClient.ownProfile.displayName;
33+
profilePicture = coreClient.ownProfile.profilePictureOption;
34+
});
35+
2936
// Listen for changes to the user's profile picture
3037
coreClient.onOwnProfileUpdate.listen((profile) {
3138
if (mounted) {

prototype/lib/conversation_list_pane/top.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ConversationListTop extends StatelessWidget {
3838
children: [
3939
UserAvatar(
4040
size: 32,
41-
username: coreClient.username,
41+
username: context.coreClient.username,
4242
image: profilePicture,
4343
onPressed: () {
4444
Navigator.push(
@@ -54,7 +54,7 @@ class ConversationListTop extends StatelessWidget {
5454
);
5555
}
5656

57-
Column _usernameSpace() {
57+
Column _usernameSpace(String username) {
5858
return Column(
5959
children: [
6060
Text(
@@ -68,7 +68,7 @@ class ConversationListTop extends StatelessWidget {
6868
),
6969
const SizedBox(height: 5),
7070
Text(
71-
coreClient.username,
71+
username,
7272
style: const TextStyle(
7373
color: colorDMB,
7474
fontSize: 10,
@@ -118,7 +118,7 @@ class ConversationListTop extends StatelessWidget {
118118
children: [
119119
_avatar(context),
120120
Expanded(
121-
child: _usernameSpace(),
121+
child: _usernameSpace(context.coreClient.username),
122122
),
123123
_settingsButton(context),
124124
],

prototype/lib/conversation_pane/conversation_content/conversation_content.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class _ConversationContentState extends State<ConversationContent> {
4141
super.initState();
4242
_scrollController.addListener(_onScroll);
4343

44+
final coreClient = context.coreClient;
4445
_conversationListener =
4546
coreClient.onConversationSwitch.listen(conversationListener);
4647
_messageListener = coreClient.onMessageUpdate.listen(messageListener);
@@ -100,7 +101,7 @@ class _ConversationContentState extends State<ConversationContent> {
100101

101102
void _onMessageVisible(String timestamp) {
102103
if (_currentConversation != null) {
103-
coreClient.user.markMessagesAsReadDebounced(
104+
context.coreClient.user.markMessagesAsReadDebounced(
104105
conversationId: _currentConversation!.id, timestamp: timestamp);
105106
}
106107
}
@@ -113,7 +114,7 @@ class _ConversationContentState extends State<ConversationContent> {
113114

114115
Future<void> updateMessages() async {
115116
if (_currentConversation != null) {
116-
final messages = await coreClient.user
117+
final messages = await context.coreClient.user
117118
.getMessages(conversationId: _currentConversation!.id, lastN: 50);
118119
setState(() {
119120
print("Number of messages: ${messages.length}");

prototype/lib/conversation_pane/conversation_content/text_message_tile.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class _TextMessageTileState extends State<TextMessageTile> {
2424
@override
2525
void initState() {
2626
super.initState();
27-
coreClient.user
27+
context.coreClient.user
2828
.userProfile(userName: widget.contentFlight.last.sender)
2929
.then((p) {
3030
if (mounted) {
@@ -36,7 +36,7 @@ class _TextMessageTileState extends State<TextMessageTile> {
3636
}
3737

3838
bool isSender() {
39-
return widget.contentFlight.last.sender == coreClient.username;
39+
return widget.contentFlight.last.sender == context.coreClient.username;
4040
}
4141

4242
@override
@@ -95,7 +95,7 @@ class _TextMessageTileState extends State<TextMessageTile> {
9595

9696
Widget _avatar() {
9797
return FutureUserAvatar(
98-
profile: coreClient.user
98+
profile: context.coreClient.user
9999
.userProfile(userName: widget.contentFlight.last.sender),
100100
);
101101
}

prototype/lib/conversation_pane/conversation_details/add_members.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class _AddMembersState extends State<AddMembers> {
3131
void initState() {
3232
super.initState();
3333
_conversationListener =
34-
coreClient.onConversationSwitch.listen(conversationListener);
34+
context.coreClient.onConversationSwitch.listen(conversationListener);
3535
getContacts();
3636
}
3737

@@ -42,13 +42,14 @@ class _AddMembersState extends State<AddMembers> {
4242
}
4343

4444
getContacts() async {
45-
contacts = await coreClient.getContacts();
45+
contacts = await context.coreClient.getContacts();
4646
setState(() {});
4747
}
4848

4949
addContacts() async {
5050
for (var contact in selectedContacts) {
51-
await coreClient.addUserToConversation(widget.conversation.id, contact);
51+
await context.coreClient
52+
.addUserToConversation(widget.conversation.id, contact);
5253
}
5354
}
5455

@@ -92,7 +93,7 @@ class _AddMembersState extends State<AddMembers> {
9293
final contact = contacts[index];
9394
return ListTile(
9495
leading: FutureUserAvatar(
95-
profile: coreClient.user
96+
profile: context.coreClient.user
9697
.userProfile(userName: contact.userName),
9798
),
9899
title: Text(

prototype/lib/conversation_pane/conversation_details/connection_details.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ConnectionDetails extends StatelessWidget {
1919

2020
@override
2121
Widget build(BuildContext context) {
22+
final coreClient = context.coreClient;
2223
return Center(
2324
child: Column(
2425
mainAxisAlignment: MainAxisAlignment.start,

0 commit comments

Comments
 (0)