flutterprovidershare-plusscreenshotexception

ProviderNotFoundException: screenshot & share an image created programmatically


this constitutes my first post here on StackOverflow. I have tried to figure the problem on my own, but I'm a new developer and even if I was to trial-and-error my way through, I don't think I'd have a good understanding of what's going wrong here.

Desired functionality here revolves around capturing a screenshot of an invisible widget in order to share it to social media platforms. Think sharing a song to your instagram story in Spotify or something.

The logic of the user journey is as follows: user ranks tiles in order to create a tier list. Upon finishing their rankings, a button appears for them to share the tiles they have put in the top tier (S-Tier):

//park_rank_view.dart

onPressed: () async {
   final image = await controller.captureFromWidget(const InvisibleWidget(), delay: const Duration(seconds: 5), context:                                                                                       context);
   // ignore: use_build_context_synchronously
  shareImage(context, image);
                                          
}

This button awaits the screenshot controller provided in the same file final controller = ScreenshotController(); then captures the InvisibleWidget (different file) & finally shares it using share_plus package:

//invisible_widget.dart

Future shareImage(BuildContext context, Uint8List bytes) async {
  final tempDir = await getApplicationDocumentsDirectory();
  final image = File('${tempDir.path}/image.png');
  // ignore: use_build_context_synchronously
  final box = context.findRenderObject() as RenderBox?;
  image.writeAsBytesSync(bytes);

  await Share.shareXFiles([XFile(image.path)],
      text: 'I just ranked the national parks!',
      sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}

class InvisibleWidget extends StatefulWidget {
  const InvisibleWidget({super.key});

  @override
  InvisibleWidgetState createState() => InvisibleWidgetState();
}

class InvisibleWidgetState extends State<InvisibleWidget> {
  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (context) {
        return Screenshot(
          controller: ScreenshotController(),
          child: Container(
            decoration: const BoxDecoration(
              image: DecorationImage(
                image: AssetImage(
                    'assets/images/background.jpg'), // Your background image
                fit: BoxFit.scaleDown,
              ),
            ),
            child: Center(
              child: SizedBox(
                height: 1050,
                width: 590,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    const Text('Check Out my S-Tier Parks 👀',
                        style: TextStyle(color: Colors.white, fontSize: 35)),
                    const SizedBox(height: 50),
                    SizedBox(
                      height: 920,
                      child: Consumer<ParkTileModel>(
                          builder: (context, tileContext, _) {
                        return GridView.count(
                          crossAxisCount: 4,
                          physics: const NeverScrollableScrollPhysics(),
                          mainAxisSpacing: 15,
                          crossAxisSpacing: 10,
                          children: tierSList(context),
                        );
                      }
                      ),
                    )
                  ],
                ),
              ),
            ),
          ),
        );
      }
    );
  }
}

This in turn builds the image based on the values provided by the user:

//build_origin.dart

List<Widget> tierSList(BuildContext context) {
  final tierS = context.watch<ParkTileModel>().tieredTiles['S'] ?? [];
  return tierS
      .map((tile) => buildOrigin(context, tile, draggable: false))
      .toList();
}

Providers have not yet been scoped, I plan to refactor later to be more efficient but for now a MultiProvider is wrapping the entire MaterialApp:

//main.dart

MultiProvider(
providers: [
            ChangeNotifierProvider<ParkTileModel>(
                create: (context) => ParkTileModel() 
                  
                ),
          ],
   // child: MaterialApp(etc, etc)
);

It should be noted that I created a route for InvisibleWidget() in order to visualize it while I was building. When I view it through the route, the widget displays perfectly with all the tiles expected. Based on this, I suppose it would follow that the inability to find the correct Provider has something to do with the screenshot method itself.

This is my first "real" project which I hope to present to potential employers and I am a bit out of my depth here. Though, I suppose the whole point is to work through these concepts and learn bit by bit. Any assistance would be greatly appreciated and let me know if I haven't conformed to best practices for StackOverflow posts. I will attach the current condition of the image which includes the bulk of the error message.

The relevant error-causing widget was: Consumer in the invisible_widget.dart file.

"Share S-Tier Parks" Button Resulting Image

Let me know if there's any more context needed to squash this one. Thanks for your time.


Solution

  • You are creating the instance of InvisibleWidget when invoking the controller.captureFromWidget, which means that the widget is not part of your widgets-tree. Consequently, its BuildContext does not have access to the providers of your app.

    One solution could be wrapping your widget around a Provider:

    onPressed: () async {
       final image = await controller.captureFromWidget(
           const Provider<ParkTileModel>.value(
                   value: context.read<ParkTileModel>(),
                   child: InvisibleWidget(),
               ), 
           delay: const Duration(seconds: 5),
           context:                                                                                       
           context,
       );
       // ignore: use_build_context_synchronously
      shareImage(context, image);
                                              
    }