flutterdartflutter-material

"No Material widget found. " at Hero image animation between routes


Flutter Newb: I have a grid of image buttons on a menu screen. When clicking a button it animates into the 'leading' element of the new view's AppBar. That works as I was expecting it to. So far so good...

When I Navigator.pop() that 2nd view I get the error "No Material widget found."... The specific widget that could not find a Material ancestor was: _InkResponseStateWidget... (though the menu screen does reappear as expected

/// ...
class MenuScreen extends StatelessWidget {
  const MenuScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Menu'),
      ),
      body: Center(
        child: GridView.count(
          crossAxisCount: 3,
          children: const [
            MenuButton('clients'),
    //...and so on

///...
class MenuButton extends StatelessWidget {
  final String indexStr;

  const MenuButton(this.indexStr, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Hero(
      child: InkWell(
        child: Ink.image(
          image: AssetImage('assets/images/$indexStr.png'),
          fit: BoxFit.cover,
        ),
        onTap: () => Navigator.pushNamed(context, '/' + indexStr),
        highlightColor: Colors.blue.withOpacity(0.5),
        splashColor: Colors.blue.withOpacity(0.9),
        borderRadius: BorderRadius.circular(100),
      ),
      tag: indexStr,
    );
  }
}

PS. I can't say I completely understand all the context and key stuff either :-)

The second 'screen's code is

import 'package:flutter/material.dart';

class ClientsScreen extends StatelessWidget {
  const ClientsScreen({Key? key}) : super(key: key);
  final indexStr = 'clients';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Material(
          child: Hero(
            child: Image.asset('../assets/images/$indexStr.png'),
            tag: indexStr,
          ),
        ),
        title: const Text('Clients.'),
      ),
      body: Center(child: Text('Test')),
      floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('Back')),
    );
  }
}

I imagine I'm missing something obvious, but can't see what it might be...

Update:, and the main.dart root app with the routes map:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes',
      theme: ThemeData(scaffoldBackgroundColor: Colors.white),
      initialRoute: '/',
      routes: {
        '/': (context) => const MenuScreen(),
        '/clients': (context) => const ClientsScreen(),
        '/checkin': (context) => const CheckinScreen(),
             },
    );
  }
}

Solution

  • As others already pointed out in the comments, a Material widget is needed to wrap the InkWell.

    Reason:

    The InkWell widget must have a Material widget as an ancestor. The Material widget is where the ink reactions are actually painted. This matches the material design premise wherein the Material is what is actually reacting to touches by spreading ink.

    https://api.flutter.dev/flutter/material/InkWell-class.html

    Ink effects: Material shows ink effects implemented by InkFeatures like InkSplash and InkHighlight below its children.

    https://api.flutter.dev/flutter/material/Material-class.html

    The Material widget displays ink effects like InkSplash and InkHighlight of its respective children. Widgets already implementing the Material functionality are Scaffold, Card etc. Which is why one of these is needed as an ancestor, and if not, Material is needed.