jhomlala / betterplayer

Better video player for Flutter, with multiple configuration options. Solving typical use cases!
Apache License 2.0
925 stars 1.01k forks source link

[BUG] Scrolling down in a ListView.builder of BetterPlayer widgets in strangely specific conditions throws "hasSize" failedAssertion #864

Open nathantew14 opened 2 years ago

nathantew14 commented 2 years ago

Describe the bug Testing on my physical and simulator iPhone, scrolling down when viewing a ListView.builder list of BetterPlayer widgets causes an error, but not on my AVD:

════════ Exception caught by gesture library ═══════════════════════════════════
The following assertion was thrown while routing a pointer event:
RenderBox was not laid out: RenderUiKitView#6b2e2 NEEDS-LAYOUT NEEDS-PAINT
'package:flutter/src/rendering/box.dart':
package:flutter/…/rendering/box.dart:1
Failed assertion: line 1927 pos 12: 'hasSize'

2

Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.md

When the exception was thrown, this was the stack
#2      RenderBox.size
package:flutter/…/rendering/box.dart:1927
#3      RenderUiKitView._handleGlobalPointerEvent
package:flutter/…/rendering/platform_view.dart:386
#4      PointerRouter._dispatch
package:flutter/…/gestures/pointer_router.dart:94
#5      PointerRouter._dispatchEventToRoutes.<anonymous closure>
package:flutter/…/gestures/pointer_router.dart:139
#6      _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:539:8)
#7      PointerRouter._dispatchEventToRoutes
package:flutter/…/gestures/pointer_router.dart:137
#8      PointerRouter.route
package:flutter/…/gestures/pointer_router.dart:129
#9      GestureBinding.handleEvent
package:flutter/…/gestures/binding.dart:439
#10     GestureBinding.dispatchEvent
package:flutter/…/gestures/binding.dart:419
#11     RendererBinding.dispatchEvent
package:flutter/…/rendering/binding.dart:322
#12     GestureBinding._handlePointerEventImmediately
package:flutter/…/gestures/binding.dart:374
#13     GestureBinding.handlePointerEvent
package:flutter/…/gestures/binding.dart:338
#14     GestureBinding._flushPointerEventQueue
package:flutter/…/gestures/binding.dart:296
#15     GestureBinding._handlePointerDataPacket
package:flutter/…/gestures/binding.dart:279
#19     _invoke1 (dart:ui/hooks.dart:169:10)
#20     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:293:7)
#21     _dispatchPointerDataPacket (dart:ui/hooks.dart:88:31)
(elided 5 frames from class _AssertionError and dart:async)

The specific conditions are in the steps to reproduce and the commented parts of the code. The described steps are all the factors I've found so far. I don't see how any of those conditions have anything to do with something having size or not, and they also don't seem to match what I managed to find online about the hasSize failedAssertion.

To Reproduce Steps to reproduce the behavior:

  1. Get the dependencies then copy, paste and run the code, nothing should break
  2. Change the itemCount to 6 or more and it breaks
  3. However, replacing ListView.builder with ListView makes it not break
  4. Returning BetterVideo() in the FutureBuilder without checking the snapshot makes it not break
  5. Removing the FutureBuilder entirely makes it not break
  6. Removing the AutomaticKeepAliveClientMixin makes it not break
  7. Using a Chewie player instead makes it not break

*Example code

import 'package:flutter/material.dart';
import 'package:better_player/better_player.dart';
import 'package:chewie/chewie.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'better player test',
      theme: ThemeData.light(),
      home: const BetterTest(),
    ),
  );
}

class BetterTest extends StatefulWidget {
  const BetterTest({Key? key}) : super(key: key);

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

class _BetterTestState extends State<BetterTest> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('better player test')),
      body:
          // ListView.builder breaks, but not ListView (below)
          ListView.builder(
        itemCount: 5, //6 or more breaks it
        itemBuilder: (BuildContext context, int index) {
          // return BetterVideo(); //removing the future makes it not break
          return FutureBuilder(
            future: Future.delayed(Duration(seconds: 1), () => 1),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              // return BetterVideo(); //uncommenting this makes it not break
              if (snapshot.hasData) {
                return BetterVideo();
              }
              return Center(child: CircularProgressIndicator());
            },
          );
        },
      ),

      //---------using ListView instead makes it not break-----------
      // ListView(
      //   children: [
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //     BetterVideo(
      //       key: UniqueKey(),
      //     ),
      //   ],
      // )
      //---------using ListView instead makes it not break-----------
    );
  }
}

class BetterVideo extends StatefulWidget {
  const BetterVideo({Key? key}) : super(key: key);

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

class _BetterVideoState extends State<BetterVideo>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  @override
  Widget build(BuildContext context) {
    super.build(context);

    //---------removing keepalive mixin makes it not break-----------
// class _BetterVideoState extends State<BetterVideo> {
//   @override
//   Widget build(BuildContext context) {
    //---------removing keepalive mixin makes it not break-----------

    return BetterPlayer.network(
        'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4');

    //--------using chewie players instead makes it not break-----------
    // return Chewie(
    //     controller: ChewieController(
    //         autoInitialize: true,
    //         videoPlayerController: VideoPlayerController.network(
    //             'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4')));
    //--------using chewie players instead makes it not break-----------
  }
}

Flutter doctor

nathan@Nathans-MBP-4 betacentre % flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.8.1, on macOS 12.0.1 21A559 darwin-x64, locale en-GB)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2020.3)
[✓] VS Code (version 1.63.2)
[✓] Connected device (3 available)

Better Player version

Smartphone (please complete the following information): Physical iPhone:

Android Emulator:

iPhone Simulator:

nathantew14 commented 2 years ago

@jhomlala sorry for the nudge