Skip to content

Commit f01ad9b

Browse files
committed
compose_box: Send "stopped typing" notices on app lifecycle updates
This is a bonus feature that covers some cases that FocusNode doesn't cover. We send a "stopped typing" notice when the app loses focus or become invisible. An example of WidgetsBindingObserver can be found here: https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html Signed-off-by: Zixuan James Li <[email protected]>
1 parent 559981d commit f01ad9b

File tree

2 files changed

+43
-1
lines changed

2 files changed

+43
-1
lines changed

lib/widgets/compose_box.dart

+22-1
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,34 @@ class _ContentInput extends StatefulWidget {
288288
State<_ContentInput> createState() => _ContentInputState();
289289
}
290290

291-
class _ContentInputState extends State<_ContentInput> {
291+
class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserver {
292292
String? _prevText;
293293

294294
@override
295295
void initState() {
296296
super.initState();
297297
widget.controller.addListener(_contentChanged);
298298
widget.focusNode.addListener(_focusChanged);
299+
// The focus node does not notify [AppLifecycleState] changes,
300+
// so we also listen if the app is visible and in focus.
301+
WidgetsBinding.instance.addObserver(this);
302+
}
303+
304+
@override
305+
void didChangeAppLifecycleState(AppLifecycleState state) {
306+
super.didChangeAppLifecycleState(state);
307+
switch (state) {
308+
case AppLifecycleState.detached:
309+
case AppLifecycleState.inactive:
310+
case AppLifecycleState.hidden:
311+
case AppLifecycleState.paused:
312+
final store = PerAccountStoreWidget.of(context);
313+
store.typingNotifier.stoppedComposing();
314+
case AppLifecycleState.resumed:
315+
// The app becoming visible and getting input focus doesn't
316+
// necessarily mean that the user started typing, so do nothing.
317+
break;
318+
}
299319
}
300320

301321
@override
@@ -313,6 +333,7 @@ class _ContentInputState extends State<_ContentInput> {
313333
void dispose() {
314334
widget.controller.removeListener(_contentChanged);
315335
widget.focusNode.removeListener(_focusChanged);
336+
WidgetsBinding.instance.removeObserver(this);
316337
super.dispose();
317338
}
318339

test/widgets/compose_box_test.dart

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
44
import 'package:file_picker/file_picker.dart';
55
import 'package:flutter_checks/flutter_checks.dart';
66
import 'package:http/http.dart' as http;
7+
import 'package:flutter/services.dart';
78
import 'package:flutter/material.dart';
89
import 'package:flutter_test/flutter_test.dart';
910
import 'package:image_picker/image_picker.dart';
@@ -330,6 +331,26 @@ void main() {
330331
// [TextEditingController.selection]. Still, we expect no
331332
// "typing started" request if the text remains the same.
332333
});
334+
335+
testWidgets('unfocusing app stops typing notification', (tester) async {
336+
Future<void> setAppLifeCycleState(AppLifecycleState state) async {
337+
// While this state lives on [ServicesBinding], testWidgets resets it
338+
// for us when the test ends:
339+
// https://github.com/flutter/flutter/blob/c78c166e3ecf963ca29ed503e710fd3c71eda5c9/packages/flutter_test/lib/src/binding.dart#L1189
340+
final ByteData? message = const StringCodec().encodeMessage(state.toString());
341+
await tester.binding.defaultBinaryMessenger
342+
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
343+
}
344+
345+
await prepareComposeBox(tester, narrow: narrow, account: account);
346+
347+
await checkStartTyping(tester, narrow);
348+
349+
connection.prepare(json: {});
350+
setAppLifeCycleState(AppLifecycleState.inactive);
351+
await tester.pump(Duration.zero);
352+
checkTypingRequest(TypingOp.stop, narrow);
353+
});
333354
});
334355

335356
group('message-send request response', () {

0 commit comments

Comments
 (0)