As mentioned in "Flutter Is Easier With A MUI 3 Inspired Design", one ends up having to write a lot of boilerplate when you want to do build something that didn't adhere to a MUI3 design. Most of the button-like elements in the app I was building didn't have Inkwells, some didn't look like a button (think a clickable Card), so I ended up having to code up a solution:

clickable.dart

import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class Clickable extends StatelessWidget { const Clickable({ super.key, required this.onPressed, required this.child, this.cursor = MaterialStateMouseCursor.clickable, this.onEnter, this.onExit, }); final Widget child; final Function() onPressed; final MaterialStateMouseCursor cursor; final PointerEnterEventListener? onEnter; final PointerExitEventListener? onExit; @override Widget build(BuildContext context) { return GestureDetector( onTap: onPressed, child: MouseRegion( cursor: cursor, onEnter: onEnter, onExit: onExit, child: child, ), ); } }
Nice, simple and re-usable.

clickable_test.dart

import 'dart:async'; import 'dart:ui'; import 'package:app/components/ui/helpers/clickable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Clickable', () { testWidgets('onPressed callback is called when tapped', (tester) async { final completer = Completer<void>(); final widget = Clickable( onPressed: completer.complete, child: Container(), ); // Pump the widget into the widget tree. await tester.pumpWidget(widget); // Find the Clickable widget in the widget tree. final finder = find.byType(Clickable); // Tap the Clickable widget. await tester.tap(finder); // Verify that the onPressed callback was called. expect(completer.isCompleted, true); }); testWidgets('onEnter callback is called when hovered', (tester) async { final completer = Completer<void>(); final widget = Container( margin: const EdgeInsets.all(100), child: Clickable( onEnter: completer.complete, onPressed: () {}, child: const SizedBox(width: 10, height: 10), ), ); // Pump the widget into the widget tree. await tester.pumpWidget(widget); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Clickable))); // Verify that the onEnter callback was called. expect(completer.isCompleted, true); }); testWidgets('onExit callback is called when hovered off', (tester) async { final completer = Completer<void>(); // Create a test widget. final widget = Container( margin: const EdgeInsets.all(100), child: Clickable( onExit: completer.complete, onPressed: () {}, child: const SizedBox(width: 10, height: 10), ), ); await tester.pumpWidget(widget); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Clickable))); await gesture.moveTo(Offset.zero); // Verify that the onExit callback was called. expect(completer.isCompleted, true); }); }); }