Fix nested scroll view can rebuild if mark dirty during scheduleWarmUpFrame (#36097)

diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart
index 982596c..72b8878 100644
--- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart
@@ -498,8 +498,15 @@
 
   bool get hasScrolledBody {
     for (_NestedScrollPosition position in _innerPositions) {
-      if (position.pixels > position.minScrollExtent)
+      // TODO(chunhtai): Replace null check with assert once
+      // https://github.com/flutter/flutter/issues/31195 is fixed.
+      if (
+        position.minScrollExtent != null &&
+        position.pixels != null &&
+        position.pixels > position.minScrollExtent
+      ) {
         return true;
+      }
     }
     return false;
   }
diff --git a/packages/flutter/test/widgets/nested_scroll_view_async_test.dart b/packages/flutter/test/widgets/nested_scroll_view_async_test.dart
new file mode 100644
index 0000000..075fc13
--- /dev/null
+++ b/packages/flutter/test/widgets/nested_scroll_view_async_test.dart
@@ -0,0 +1,72 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/gestures.dart' show DragStartBehavior;
+import 'package:quiver/testing/async.dart';
+
+void main() {
+  setUp(() {
+    WidgetsFlutterBinding.ensureInitialized();
+    WidgetsBinding.instance.resetEpoch();
+  });
+
+  test('NestedScrollView can build sccessfully if mark dirty during warm up frame', () {
+    final FakeAsync fakeAsync = FakeAsync();
+    fakeAsync.run((FakeAsync async) {
+      runApp(
+        MaterialApp(
+          home: Material(
+            child: DefaultTabController(
+              length: 1,
+              child: NestedScrollView(
+                dragStartBehavior: DragStartBehavior.down,
+                headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
+                  return <Widget>[
+                    const SliverPersistentHeader(
+                      delegate: TestHeader(),
+                    ),
+                  ];
+                },
+                body: SingleChildScrollView(
+                  dragStartBehavior: DragStartBehavior.down,
+                  child: Container(
+                    height: 1000.0,
+                    child: const Placeholder(),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+      // Marks element as dirty right before the first draw frame is called.
+      // This can happen when engine flush user setting.
+      final Element element = find.byType(NestedScrollView, skipOffstage: false).evaluate().single;
+      element.markNeedsBuild();
+      // Triggers draw frame timer scheduled in scheduleWarmUpFrame.
+      fakeAsync.flushTimers();
+    });
+    // Make sure widget is rebuilt correctly.
+    expect(
+      find.byType(NestedScrollView, skipOffstage: false).evaluate().single.widget is NestedScrollView,
+      isTrue
+    );
+  });
+}
+
+class TestHeader extends SliverPersistentHeaderDelegate {
+  const TestHeader();
+  @override
+  double get minExtent => 100.0;
+  @override
+  double get maxExtent => 100.0;
+  @override
+  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+    return const Placeholder();
+  }
+  @override
+  bool shouldRebuild(TestHeader oldDelegate) => false;
+}