Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(android only) PdfView becomes blank on device rotation (portrait -> landscape) #9

Open
egodefroy opened this issue May 13, 2019 · 41 comments
Labels
help wanted Extra attention is needed

Comments

@egodefroy
Copy link

(android only) PdfView becomes blank on device rotation (portrait -> landscape). Does not reappear when the device is rotated back to portrait.

This does not seem to happen when PdfView is in a SizedBox with limited height.

@endigo endigo added the help wanted Extra attention is needed label May 16, 2019
@zubyf09
Copy link

zubyf09 commented May 19, 2019

Same Issue any solution?

@egodefroy
Copy link
Author

This happens with the example posted with the package.
Just play the example and rotate the terminal

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel master, v1.6.1-pre.32, on Mac OS X 10.13.6 17G7024, locale
    fr-FR)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
[✓] Android Studio (version 3.4)
[✓] VS Code (version 1.34.0)
[✓] Connected device (3 available)

• No issues found!

@endigo
Copy link
Owner

endigo commented May 22, 2019

Hello guys, this is a limitation from a native plugin.
Related to DImuthuUpe/AndroidPdfViewer#207
So, you guys try to re-render on device rotate.

@egodefroy
Copy link
Author

It works if on 'didChangeMetrics' we redefine a new PDFViewController (no more final) and force redrawing the PDFView.

@endigo
Copy link
Owner

endigo commented May 22, 2019

@egodefroy post some example code here.

Thanks.

@l-k22
Copy link

l-k22 commented May 30, 2019

didChangeMetrics

Could you provide an example, removing final solved another issue for me that caused a blank screen but implementing the didChangeMetrics method hasn't solved the rotation issue. (I'm also experiencing the issue whenever the keyboard is displayed).

class _PDFScreenState extends State<PDFScreen> with WidgetsBindingObserver {
Completer<PDFViewController> _controller = Completer<PDFViewController>();
PDFViewController _pdfView;
......
@override
  void didChangeMetrics() {
    setState(() { 
      _controller = new Completer<PDFViewController>();
      });
  }
}

EDIT: Adding "resizeToAvoidBottomPadding : false" to the scaffold solved the blank screen when keypad appears issue.

@kunalshah
Copy link

I get the same error. Here is the log:

E/MethodChannel#plugins.endigo.io/pdfview_0(22432): Failed to handle method call
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): java.lang.IllegalStateException: Reply already submitted
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.flutter.embedding.engine.dart.DartMessenger$Reply.reply(DartMessenger.java:124)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler$1.success(MethodChannel.java:204)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.endigo.plugins.pdfviewflutter.FlutterPDFView.setPage(FlutterPDFView.java:119)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.endigo.plugins.pdfviewflutter.FlutterPDFView.onMethodCall(FlutterPDFView.java:102)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:201)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(DartMessenger.java:88)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(FlutterJNI.java:219)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at android.os.MessageQueue.nativePollOnce(Native Method)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at android.os.MessageQueue.next(MessageQueue.java:356)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at android.os.Looper.loop(Looper.java:138)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at android.app.ActivityThread.main(ActivityThread.java:6517)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at java.lang.reflect.Method.invoke(Native Method)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:942)
E/MethodChannel#plugins.endigo.io/pdfview_0(22432): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:832)
E/DartMessenger(22432): Uncaught exception in binary message listener
E/DartMessenger(22432): java.lang.IllegalStateException: Reply already submitted
E/DartMessenger(22432): 	at io.flutter.embedding.engine.dart.DartMessenger$Reply.reply(DartMessenger.java:124)
E/DartMessenger(22432): 	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:219)
E/DartMessenger(22432): 	at io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(DartMessenger.java:88)
E/DartMessenger(22432): 	at io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(FlutterJNI.java:219)
E/DartMessenger(22432): 	at android.os.MessageQueue.nativePollOnce(Native Method)
E/DartMessenger(22432): 	at android.os.MessageQueue.next(MessageQueue.java:356)
E/DartMessenger(22432): 	at android.os.Looper.loop(Looper.java:138)
E/DartMessenger(22432): 	at android.app.ActivityThread.main(ActivityThread.java:6517)
E/DartMessenger(22432): 	at java.lang.reflect.Method.invoke(Native Method)
E/DartMessenger(22432): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:942)
E/DartMessenger(22432): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:832)

Here is the repo: https://github.com/iampawan/FlutterPDFViewer

@MichelMelhem
Copy link

MichelMelhem commented Jun 27, 2019

i get the same error,
any work around ?

@l-k22
Copy link

l-k22 commented Jun 27, 2019

My work around (not really a work around) is to disable screen rotation whilst viewing PDFs
I have a single widget that handles all my PDFs so I add the following

@override
void initState(){
  super.initState();
//Portrait mode only
  SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
  ]);
}
@override
dispose(){
//on dispose allow full orientation
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeRight,
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);
  super.dispose();
}

However, according to some users on StackOverflow this doesn't work on iPads https://stackoverflow.com/a/50322184/469335

@egodefroy
Copy link
Author

My work around (not very beautiful...) is based on reinstanciating the controller in didChangeMetrics.

@override
  void didChangeMetrics() {
    if (Platform.isAndroid) {
      // for rotations on Android
      pdfController = Completer<PDFViewController>();
      pdfReload = true;
    }
  }

and use a Timer in build to force redraw the pdf (code to adapt to your special case)

    if (pdfReload) {
      // for rotations on Android
      Timer(Duration(milliseconds: 300), () {
        setState(() {
          pdfReload = false;
         });
      });
    }
...
    var stack = Stack(children: <Widget>[
      pdfReload
          ? Container() // for rotations on Android, force recreate the PDFView
          : PDFView(
              filePath: filePath,
              enableSwipe: true,
              swipeHorizontal: true,
              autoSpacing: false,
              pageFling: false,
              onRender: (_pages) {
                 setState(() {
                  pdfPages = _pages;
                  pdfIsReady = true;
                  pdfReload = false;
                });
              },
              onError: (error) {
               },
              onPageError: (page, error) {
                },
              onViewCreated: (PDFViewController pdfViewController) {
                pdfController.complete(pdfViewController);
              },
              onPageChanged: (int page, int total) {
              },
            ),
      !pdfIsReady
          ? Center(
              child: CircularProgressIndicator(),
            )
          : Container()
    ]);

@MichelMelhem
Copy link

it will be very nice if @endigo fix this issue

@MichelMelhem
Copy link

do you know any pdf viewer out there that is more maintained than this one.
Beacause i didn't find any Pdf viewer that has the same ability.

@deakjahn
Copy link
Contributor

deakjahn commented Jul 5, 2019

You shouldn't expect this automatically from this plugin, the underlying viewer clearly says it's the responsibility of the user to redraw with the current page stored earlier.

So, adding a new channel method to the controller helps us replenish the view:

void reload(MethodCall call, Result result) {
    String path = (String)call.argument("filePath");
    int page = (int)call.argument("page");
    pdfView.recycle();
    pdfView.fromFile(path).defaultPage(page).load();
    result.success(true);
}

(Note that there is a fresh regression [https://github.com/flutter/flutter/issues/33866] that causes these calls to drop java.lang.IllegalStateException: Reply already submitted errors into logcat, it will be fixed in a coming Flutter release).

And then:

@override
void didChangeMetrics() {
  if (Platform.isAndroid) {
    Future.delayed(Duration(milliseconds: 500), () {
      _controller?.reload(_data, _current);
    });
  }
}

Keep track of _current in the onPageChanged callback.

@GENL
Copy link

GENL commented Sep 18, 2019

That's how I solved the problem. I check when the app rotation changes, and I replace the current pdf viewer by a new one. You can also save the current page and redirect the viewer to that page once the viewer has been rebuilt. This is useful when the viewer page is a separated widget. Hope it helped!

class PdfViewerPage extends StatefulWidget {
  _PdfViewerPageState createState() => new _PdfViewerPageState();
}

class _PdfViewerPageState extends State<PdfViewerPage> {
  Orientation _lastScreenOrientation;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _lastScreenOrientation = MediaQuery.of(context).orientation;
    });
  }

  @override
  Widget build(BuildContext context) {
    // We need to erase and rebuild the viewer when the user changes the screen orientation.
    if (_lastScreenOrientation != null && _lastScreenOrientation != MediaQuery.of(context).orientation) {
      // Completely render the page.
      Future.delayed(Duration(microseconds: 100), _repushViewer);
    }
    return Scaffold(
      resizeToAvoidBottomPadding : false,
      body: PDFView(
        filePath: 'YOUR PDF FILE PATH.',
        onError: (e) {},
      )
    );
  }

  /// Push and replace the viewer page so the page is completely new.
  _repushViewer() {
    Navigator.of(context).pushReplacementNamed(/*YOUR PDF VIEWER PAGE ROUTE */);
  }
}

@isorensen
Copy link

Facing the same issue around here...

@endigo endigo added the hacktoberfest Hacktoberfest2019 label Oct 3, 2019
@zaheddec
Copy link

zaheddec commented Nov 15, 2019

Initially, I thought of not using it but worked around on @GENL's comment. Look like we got it working except for changing the default page to the last page before rotation.

@zaheddec
Copy link

d I replace the current pdf viewer by a new one

How can we redirect the viewer to the current page once the viewer has been rebuilt? Appreciate any idea around it.

@endigo
Copy link
Owner

endigo commented Nov 18, 2019

@zaheddec hello, you can use setPage function.

controller.setPage(current);

@zaheddec
Copy link

zaheddec commented Nov 18, 2019

@endigo Thanks for the replay. I tried to play around with it but could not get any success with it.

  1. I tried to await _controller.future.then((onValue)=>onValue.setPage(5)); in onViewCreated after completer.
  2. Tried to add it to onRender callback too but no success.

would really appreciate any idea where to place it.

Thanks again

Note: I called the setpage() in onredender() and it all worked.

@zaheddec
Copy link

@endigo @GENL Thanks you so much guys. Finally, all worked and set to go for production. Great work, great lightweight library, and great support.

@GENL
Copy link

GENL commented Nov 18, 2019

Nice to have helped

@isorensen
Copy link

isorensen commented Jan 4, 2020

Dear @zaheddec , how did you replace your hardcoded "5" page to the current last page? I tried to set 'currentPage = page' inside onPageChanged, but it resets when the page is refreshed.
Thank you, and also thanks to @endigo and @GENL for suggesting the code

@endigo endigo removed the hacktoberfest Hacktoberfest2019 label Mar 30, 2020
@redbayoub
Copy link

My workaround with the help of @GENL comment.


 Orientation _lastScreenOrientation;
 UniqueKey pdfViewerKey = UniqueKey();

@override
  Widget build(BuildContext context) {
 return OrientationBuilder(
                  builder: (ctx, ori) {
                    Orientation newOrientation =
                        MediaQuery.of(context).orientation;
                    if (_lastScreenOrientation != null &&
                        _lastScreenOrientation != newOrientation) {
                      Future.delayed(Duration(microseconds: 100), () {
                        setState(() {
                          _lastScreenOrientation = newOrientation;
                          pdfViewerKey = UniqueKey();
                        });
                      });
                    }
                    return PDFView(
                      key: pdfViewerKey,
                      filePath: cachedFilePath,
                      onPageChanged: (page, total) {
                        setState(() {
                          currPage = page;
                          totalPages = total;
                        });
                      },
                      pageFling: true,
                      onViewCreated: (controller) {
                        if (currPage != null) controller.setPage(currPage);
                      },
                    );
                  },
                );
}

but i still don't know why controller.setPage(currPage) not working
and when orientation changes i get
W/System ( 4490): A resource failed to call release.
but it's working

@akshayyadav76
Copy link

My workaround with the help of @GENL comment.


 Orientation _lastScreenOrientation;
 UniqueKey pdfViewerKey = UniqueKey();

@override
  Widget build(BuildContext context) {
 return OrientationBuilder(
                  builder: (ctx, ori) {
                    Orientation newOrientation =
                        MediaQuery.of(context).orientation;
                    if (_lastScreenOrientation != null &&
                        _lastScreenOrientation != newOrientation) {
                      Future.delayed(Duration(microseconds: 100), () {
                        setState(() {
                          _lastScreenOrientation = newOrientation;
                          pdfViewerKey = UniqueKey();
                        });
                      });
                    }
                    return PDFView(
                      key: pdfViewerKey,
                      filePath: cachedFilePath,
                      onPageChanged: (page, total) {
                        setState(() {
                          currPage = page;
                          totalPages = total;
                        });
                      },
                      pageFling: true,
                      onViewCreated: (controller) {
                        if (currPage != null) controller.setPage(currPage);
                      },
                    );
                  },
                );
}

but i still don't know why controller.setPage(currPage) not working
and when orientation changes i get
W/System ( 4490): A resource failed to call release.
but it's working

because, your setting page in a wrong way...

@akshayyadav76
Copy link

akshayyadav76 commented Apr 23, 2020

bool stopBuild = true;
 bool stopBuild2 = false;
 UniqueKey pdfViewerKey = UniqueKey();

Widget build(BuildContext context) {
   final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
   if (isLandscape) {
     if (stopBuild) {
       setState(() {
         _controller = Completer<PDFViewController>();
         pdfViewerKey = UniqueKey();
         stopBuild = false;
         stopBuild2 = true;
       });
     }
   }
   if (!isLandscape) {
     if (stopBuild2) {
       setState(() {
         _controller = Completer<PDFViewController>();
         pdfViewerKey = UniqueKey();
         stopBuild2 = false;
         stopBuild = true;
       });
     }
return pdfView(
key pdfViewerKey ,);
   }



//// when you setPage use do like that

FutureBuilder<PDFViewController>(
                   future: _controller.future,
                   builder:
                       (context, AsyncSnapshot<PDFViewController> snapshot) {
                     if (snapshot.hasData) {
                       return GestureDetector(
                           onTap: () {
                             snapshot.data.setPage(pages);
                           },
                           child: Text("end"));
                     }
                     return Container();
                   },
                 ),
               ),

well you can try like this
it's working perfectly fine for me portrait and landscape even night mode also......
if you want you can check file repo -: college_books/pdfScreen.dart

@deakjahn
Copy link
Contributor

deakjahn commented Apr 29, 2020

I suggest a combination of my previous suggestion and the latest ones. No need to track the orientation, or to use an OrientationBuilder, or to force the operation on any other platform than Android:

The only thing needed is:

class XXXState extends State<XXX> with WidgetsBindingObserver {
  var pdfViewerKey = UniqueKey();

@override
void initState() {
  super.initState();
  WidgetsBinding.instance!.addObserver(this);
}

@override
void dispose() {
  WidgetsBinding.instance!.removeObserver(this);
  super.dispose();
}

@override
void didChangeMetrics() {
  if (defaultTargetPlatform == TargetPlatform.android) {
    Future.delayed(Duration(milliseconds: 250), () {
      setState(() => pdfViewerKey = UniqueKey());
    });
  }
}

and to use the pdfViewerKey when building the PDFView. This will recreate it when the need arises.

The only remaining problem is the slight black flicker when the PlatformView rebuilds but that seems to come from Flutter itself, it's mentioned here and there.

@egodefroy
Copy link
Author

@deakjahn
This is close to the work-around I suggested on 27 Jun 2019. A bit simpler though ;-)

@deakjahn
Copy link
Contributor

Yep, and also similar to my first idea from last July. That's why I said combination. :-)

@redbayoub
Copy link

redbayoub commented Apr 30, 2020

@akshayyadav76 thanks for your solution it work perfectly for me ,but for fit policy i used the default one ( didn't use this option at all ) and it looks great

@deakjahn i tried your solution but it didn't work for me, as soon i change the orientation the pages doesn't show up

@deakjahn
Copy link
Contributor

@redbayoub I modified the suggestion, try that. It seems that the rest is also needed to make didChangeMetrics() fire.

@chrislaurie-io
Copy link

I am trying to implement the solution suggested by @deakjahn - this in null safety. I have a problem with this line:

WidgetsBinding.instance.addObserver(this);

I am getting the following error: The method 'addObserver' can't be unconditionally invoked because the receiver can be null.

The didChangeMetrics method is never called when going from portrait to landscape.

@deakjahn
Copy link
Contributor

If you're positive that it'll never be null, you use a !:

WidgetsBinding.instance!.addObserver(this);

If you want to leave it open to not to crash if it is, you use a ?:

WidgetsBinding.instance?.addObserver(this);

But this isn't PdfView-specific, just the new null-safe Dart, so it might be best for you to read it up. :-))

@jason19970210
Copy link

@deakjahn

Hello there, I am trying to implement your method since I modify my pdf viewer page from stateless to stateful.
But once I am working on didChangeMetrics then I have no idea where I should declare pdfViewerKey and how to use it !!

Or could you making a example for this great method for avoid pdf runs away once android is rotated.

Thanks for help !!

@deakjahn
Copy link
Contributor

deakjahn commented Dec 17, 2021

Just declare it inside your state. I modified the original post with that line.

You don't have to do any more than what's in that snippet. On metrics change, a new uniqe key will be created, and this will force Flutter to create a new PDF viewer instead of reusing the previous one. Flutter automatically rebuilds any widget when the key changes (state is actually linked to the key inside Flutter, so a new key means a new state).

@mg142jf
Copy link

mg142jf commented May 12, 2022

I tried my best but i did not figure it out,
Could any one just write the full widget with all the right settings.

@soni4kirtan6
Copy link

@deakjahn, I am not able to understand where to write reload() function?

You shouldn't expect this automatically from this plugin, the underlying viewer clearly says it's the responsibility of the user to redraw with the current page stored earlier.

So, adding a new channel method to the controller helps us replenish the view:

void reload(MethodCall call, Result result) {
    String path = (String)call.argument("filePath");
    int page = (int)call.argument("page");
    pdfView.recycle();
    pdfView.fromFile(path).defaultPage(page).load();
    result.success(true);
}

(Note that there is a fresh regression [https://github.com/flutter/flutter/issues/33866] that causes these calls to drop java.lang.IllegalStateException: Reply already submitted errors into logcat, it will be fixed in a coming Flutter release).

And then:

@override
void didChangeMetrics() {
  if (Platform.isAndroid) {
    Future.delayed(Duration(milliseconds: 500), () {
      _controller?.reload(_data, _current);
    });
  }
}

Keep track of _current in the onPageChanged callback.

@deakjahn
Copy link
Contributor

I no longer use endigo's plugin because I needed more and different features (eg. a way to show page pairs at will) but as far as I can remember, I had another suggestion later: #9 (comment)

These said there's no need for that reload() any more. You only need what's in that later comment, simply to provide a key to the viewer and change that key when the orientation changes. This will already force the system to recreate the view in the platform part automatically.

@AmirSarraf
Copy link

AmirSarraf commented Jul 7, 2022

this forces flutter to rebuild the widget, so it works

return OrientationBuilder(
    builder: (context, orientation) {
  return PDFView(
    key: ValueKey('pdf_local_$orientation'),
    filePath: path_to_pdf,
  );
});

@alexaung
Copy link

Showing four page when turn from portrait to landscape.
viber_image_2022-08-16_22-31-02-914

@myselfuser1
Copy link

This will help https://www.youtube.com/watch?v=gAUVz0U7eyA

@sjmamani
Copy link

sjmamani commented May 30, 2024

In my case, my PdfView becomes blank when the system puts the app in the background or returns the app to the foreground. To resolve that I add didChangeAppLifecycleState instead of didChangeMetrics method to my StatefulWidget:

@override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (defaultTargetPlatform == TargetPlatform.android &&
        state == AppLifecycleState.resumed) {
      setState(() => pdfViewerKey = UniqueKey());
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests