hm21 / pro_image_editor

The pro_image_editor is a Flutter widget designed for image editing within your application. It provides a flexible and convenient way to integrate image editing capabilities into your Flutter project.
https://hm21.github.io/pro_image_editor/
BSD 3-Clause "New" or "Revised" License
94 stars 59 forks source link

[Bug]: Unable to close loading dialog in CropRotateEditor.memory and ProImageEditor.memory #184

Closed gd08xxx closed 1 month ago

gd08xxx commented 1 month ago

Package Version

^4.3.3

Flutter Version

3.22.2

Platforms

Android, iOS

Screenshot 2024-08-03 at 03 40 40 Screenshot 2024-08-03 at 03 40 56

How to reproduce?

I want the user to capture/select an image, navigate to a crop screen (forcing it to crop 16:9), navigate to an edit screen to let the user edit the image and show a preview screen.

There is a problem with the loading dialog that comes with the package.

What I expect: 1) Capture/select image 2) Navigate to CropScreen(force 16:9) 3) Finished cropping and press check mark on top right [Loading shows and dismiss] 4) Navigate to EditScreen with onImageEditingComplete 5) Finished editing and press checkmark on top right [Loading shows and dismiss] 6) Navigate to PreviewScreen with onImageEditingComplete

What happened: 1) Capture/select image 2) Navigate to CropScreen(force 16:9) 3) Finished cropping and press check mark on top right [Loading keeps showing] 4) Navigate to EditScreen with onImageEditingComplete 5) Need to dismiss loading dialog before can interact with EditScreen 6) Finished editing and press checkmark on top right [Loading keeps showing] 7) Navigate to PreviewScreen with onImageEditingComplete 8) Need to dismiss loading dialog before can interact with PreviewScreen

I saw the readme file regarding async in onImageEditingComplete and the loading dialog, however my IDE forces me to use async because your return type is Future Function(Uint8List bytes)

Logs (optional)

No response

Example code (optional)

I use the go_router package, but I tried using Navigator.of(context).push() and it doesn't fix the problem

CropScreen buildBody()

CropRotateEditor.memory(
        _image,
        initConfigs: CropRotateEditorInitConfigs(
          theme: ThemeData.dark(),
          convertToUint8List: true,
          configs: const ProImageEditorConfigs(
            cropRotateEditorConfigs: CropRotateEditorConfigs(
              initAspectRatio: 16 / 9,
              canChangeAspectRatio: false,
              canRotate: false,
            ),
          ),
          onImageEditingComplete: (Uint8List image) async {
            await context.pushNamed(
              HostingChallengeCoverImageImageEditorScreen.path,
              extra: HostingChallengeCoverImageEditorScreenArgs(
                _title,
                _description,
                _deadline,
                _hasRewards,
                _message,
                image,
              ),
            );
          },
        ),
      )

EditScreen buildBody()

ProImageEditor.memory(
        widget._image,
        configs: const ProImageEditorConfigs(
          cropRotateEditorConfigs: CropRotateEditorConfigs(enabled: false),
        ),
        callbacks: ProImageEditorCallbacks(
          onImageEditingComplete: (Uint8List editedImage) async {
            final String coverImagePublicId;
            const String prefix = 'challenge-cover-image';
            try {
              coverImagePublicId = await _repository.postFile(
                editedImage.toFile(
                  prefix: prefix,
                  //pro_image_editor's default format
                  extension: 'jpg',
                ),
              );
            } on Exception catch (e) {
              if (!context.mounted) return;
              await showPostFileExceptionMixAlertDialog(
                context,
                filePrefix: prefix,
                exception: e,
              );
              return;
            }
            if (!context.mounted) return;
            await context.pushNamed(
              HostingChallengePreviewScreen.path,
              extra: HostingChallengePreviewScreenArgs(
                coverImagePublicId,
                widget._title,
                widget._description,
                widget._deadline,
                widget._hasRewards,
                widget._message,
              ),
            );
          },
        ),
      )


### Device Model (optional)

_No response_
hm21 commented 1 month ago

Hi,

The problem is that when you open the editor as a new page, it's required that you call Navigator.pop inside the onImageEditingComplete function. The new page should be opened inside onCloseEditor so that the loading-dialog does not close it directly. I know it's sometimes a bit confusing why the loading-dialog closes after the onImageEditingComplete function has been called. The idea here is that when you upload the final image to your server, which will definitely take some time, the same loading-dialog will still be active for the generation and upload part.

Anyway, I had a small issue with the loading-dialog that it was close to early, but in my tests it didn't affect anything. However, that everything will work fine, I still recommend you to update to version 4.3.4.

Below is a demo code but without the package go_router. Please let me know it in the case it didn't work for you.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pro_image_editor/pro_image_editor.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pro-Image-Editor',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade800),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Uint8List? _image;

  _openCropRotateEditor() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) {
          return CropRotateEditor.network(
            'https://picsum.photos/id/230/2000',
            initConfigs: CropRotateEditorInitConfigs(
              theme: ThemeData.dark(),
              convertToUint8List: true,
              configs: const ProImageEditorConfigs(
                cropRotateEditorConfigs: CropRotateEditorConfigs(
                  initAspectRatio: 16 / 9,
                  canChangeAspectRatio: false,
                  canRotate: false,
                ),
              ),
              onImageEditingComplete: (Uint8List image) async {
                _image = image;
                Navigator.pop(context);
              },
              onCloseEditor: () {
                if (_image != null) _openMainEditor();
              },
            ),
          );
        },
      ),
    );
  }

  _openMainEditor() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) {
          return ProImageEditor.memory(
            _image!,
            configs: const ProImageEditorConfigs(
              cropRotateEditorConfigs: CropRotateEditorConfigs(enabled: false),
            ),
            callbacks: ProImageEditorCallbacks(
              onImageEditingComplete: (Uint8List image) async {
                _image = image;
                Navigator.pop(context);

                /// TODO: Upload your image...
              },
              onCloseEditor: () {
                if (_image != null) _openPreviewEditor();
              },
            ),
          );
        },
      ),
    );
  }

  _openPreviewEditor() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) {
          return Scaffold(
            appBar: AppBar(
              title: const Text('Preview'),
            ),
            body: Center(
              child: Image.memory(_image!),
            ),
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FilledButton(
          onPressed: _openCropRotateEditor,
          child: const Text('Test'),
        ),
      ),
    );
  }
}
gd08xxx commented 1 month ago

Thanks a lot for your detailed explanation, I've spent lots of time trying out various scenarios and your example alone worked for the forward flow, however the back flow does not work. I upgraded to ^4.3.5 and edited my code to something similar to your's. It worked 1/10 times, which is very strange.

I suspect the issue is from here in the source code, where you actually hide the dialog if it is mounted, however I have already navigated to a new screen and the mount state has changed:


    WidgetsBinding.instance.addPostFrameCallback((_) async {
      LoadingDialog loading = LoadingDialog();
      await loading.show(
        context,
        theme: _theme,
        configs: configs,
        message: i18n.doneLoadingMsg,
      );

      if (callbacks.onThumbnailGenerated != null) {
        if (_imageInfos == null) await decodeImage();

        final List<dynamic> results = await Future.wait([
          captureEditorImage(),
          _controllers.screenshot.getOriginalImage(imageInfos: _imageInfos!),
        ]);

        await callbacks.onThumbnailGenerated!(results[0], results[1]);
      } else {
        Uint8List? bytes = await captureEditorImage();
        await onImageEditingComplete?.call(bytes);
      }

      **if (mounted) loading.hide(context);**

      onCloseEditor?.call();

      /// Allow users to continue editing if they didn't close the editor.
      setState(() => _processFinalImage = false);
    });

Or maybe the issue is due to async and await, where I noticed that you do not use "await" before Navigator.push() in the example that you have given me. In my scenario, using or not using "await" Navigator.push() with GoRoute.push both does not work for me all the time.

As a workaround, I used SizedBox.shrink() as my ImageEditorCustomWidgets.loadingDialog, is there a way to totally disable the LoadingDialog? (Setting ImageEditorCustomWidgets.loadingDialog = null uses the default dialog) My workaround now requires the user to tap on the screen twice to interact with it, because he/she has to disable the invisible custom SizedBox.shrink().

hm21 commented 1 month ago

Thanks a lot for your detailed explanation, I've spent lots of time trying out various scenarios and your example alone worked for the forward flow, however the back flow does not work.

To clarify, the issues is only when navigating back from the preview or main-editor page, correct? Navigating back is not straightforward because the editor is designed to be destroyed upon leaving the page. If you keep the editor active and do not call Navigator.pop inside onImageEditingComplete, you will have many background isolates or web workers running unnecessarily. Therefore, if the user wants to navigate back, I recommend having them restart the entire process or use import/export functionality to save the state history and transformConfigs to create a new editor instance.

I suspect the issue is from here in the source code, where you actually hide the dialog if it is mounted, however I have already navigated to a new screen and the mount state has changed:

The code will only run when generating the final image, but that should not be a problem. As you can see, I call onCloseEditor after checking if(mounted) loading.hide(context), so when you open the new editor inside onCloseEditor, if (mounted) should still evaluate to true.

Or maybe the issue is due to async and await, where I noticed that you do not use "await" before Navigator.push() in the example that you have given me. In my scenario, using or not using "await" Navigator.push() with GoRoute.push both does not work for me all the time.

Yes, it should not be necessary to await call Navigator.push because no other code will be executed which needs to await that task.

As a workaround, I used SizedBox.shrink() as my ImageEditorCustomWidgets.loadingDialog, is there a way to totally disable the LoadingDialog?

No, that's not possible because it's necessary that the user can't interact with the editor until we're done creating the final image.


Could you create a single-file example similar to the one I provided above, using the code you are currently trying to implement? If the example requires additional packages, please include the versions you are using. I can then help guide you toward a working solution. Keep in mind to just post code, which is really necessary.

gd08xxx commented 1 month ago

@hm21 I upgraded the package to ^5.0.2 and everything worked fine! Thanks a lot for maintaining this package!