skip to Main Content

I’ve encountered a weird issue in my Flutter code that I will try to describe – maybe somebody will have an answer for that.

I have a ListView (called _MessageList) with chat bubbles that I want to scroll to the bottom of. This ListView is nested inside my custom Scaffold with AppBar widget called ScreenWithAppBar. I have added a ScrollController to the ScreenWithAppBar and added the method below to the initState method of the chat screen.

SchedulerBinding.instance.addPostFrameCallback(
          (_) {
        scrollController.animateTo(
          scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 2000),
          curve: Curves.easeOut,
        );
      },
    );

After entering the screen however, it only scrolls a little tiny bit until the app bar disappears and then stops. I have tried adding the scroll controller to the ListView alone itself, but it does NOT work.

Here is a gif of my issue:
gif

What can I do to scroll to the bottom of the screen?
I am attaching the whole code for the chat widgets and also for my ScreenWithAppBar widget.

ChatScreen widget – this is a screen that contains the list with chat bubbles:

class ChatScreen extends StatefulWidget {
  const ChatScreen({
    super.key,
    required this.room,
  });

  final RoomModel room;

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  late TextEditingController messageController;
  late ScrollController scrollController;
  late String userId;

  @override
  void initState() {
    super.initState();
    messageController = TextEditingController();
    scrollController = ScrollController();
    userId = context.read<UserCubit>().getCurrentUserId();
    SchedulerBinding.instance.addPostFrameCallback(
          (_) {
        scrollController.animateTo(
          scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return ScreenWithAppBar(
      scrollController: scrollController,
      title: widget.room.users.firstWhere(
        (String element) => element != userId,
      ),
      bottomNavigationBar: _MessageInputBar(
        messageController: messageController,
        scrollController: scrollController,
        room: widget.room,
        userId: userId,
      ),
      body: StreamBuilder(
        initialData: widget.room,
        stream: FirebaseUtils.firestore.getSnapshotStream(
          path: '/chatRooms',
          uid: widget.room.uid,
        ),
        builder: (BuildContext context, AsyncSnapshot<Object> snapshot) {
          return StreamBuilder<List<MessageModel>>(
            stream: context.read<MessageCubit>().getMessageStream(
                  room: widget.room,
                ),
            builder: (
              BuildContext context,
              AsyncSnapshot<List<MessageModel>> snapshot,
            ) {
              if (snapshot.hasData) {
                return _MessageList(
                  messageList: snapshot.data!,
                );
              } else {
                return const Center(
                  child: CircularProgressIndicator(),
                );
              }
            },
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    messageController.dispose();
    scrollController.dispose();
    super.dispose();
  }
}

_MessageList – widget that contains a ListView that generates the chat bubbles:

class _MessageList extends StatelessWidget {
  const _MessageList({
    required this.messageList,
  });

  final List<MessageModel> messageList;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      itemCount: messageList.length,
      itemBuilder: (BuildContext context, int index) {
        final MessageModel message = messageList[index];
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: _MessageBubble(
            message: message,
          ),
        );
      },
    );
  }
}

ScreenWithAppBar – a custom built generic entry screen that I use everywhere:

class ScreenWithAppBar extends StatelessWidget {
  const ScreenWithAppBar({
    this.showAppBar = true,
    this.title,
    this.centerTitle = false,
    this.appBarTransparent = true,
    this.appBarShowBackButton = true,
    this.actions,
    this.bottom,
    this.backButtonAlwaysEnabled = false,
    this.body,
    this.customPadding = 0.0,
    this.physics = const BouncingScrollPhysics(),
    this.isBodyScrollable = true,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.scrollController,
    this.fab,
    this.fabLocation = FloatingActionButtonLocation.endFloat,
    this.bottomNavigationBar,
    this.onWillPop,
    this.onRefresh,
    this.notificationPredicate,
    this.isHome = false,
    this.isPrimaryColor = false,
    this.isNestedScrollEnabled = true,
    this.resizeToAvoidBottomInset = true,
    this.usePageView = false,
    this.pageViewController,
    this.pageViewChildren,
    super.key,
  }) : assert(!backButtonAlwaysEnabled || onWillPop != null);

  /// App Bar
  final bool showAppBar;
  final String? title;
  final bool centerTitle;
  final bool appBarTransparent;
  final bool appBarShowBackButton;
  final List<Widget>? actions;
  final PreferredSizeWidget? bottom;
  final bool backButtonAlwaysEnabled;

  /// Body
  final Widget? body;
  final double customPadding;
  final ScrollPhysics physics;
  final bool isBodyScrollable;
  final bool extendBody;
  final bool extendBodyBehindAppBar;
  final ScrollController? scrollController;

  /// Fab
  final Widget? fab;
  final FloatingActionButtonLocation fabLocation;

  /// BottomNavBar
  final Widget? bottomNavigationBar;

  /// Callbacks
  final Future<bool> Function()? onWillPop;
  final Future<void> Function()? onRefresh;
  final bool Function(ScrollNotification)? notificationPredicate;

  /// Flags
  final bool isHome;
  final bool isPrimaryColor;
  final bool isNestedScrollEnabled;
  final bool resizeToAvoidBottomInset;

  /// PageView
  final bool usePageView;
  final PageController? pageViewController;
  final List<Widget>? pageViewChildren;

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: onWillPop,
      child: Scaffold(
        extendBody: extendBody,
        extendBodyBehindAppBar: extendBodyBehindAppBar,
        resizeToAvoidBottomInset: resizeToAvoidBottomInset,
        appBar: isNestedScrollEnabled
            ? null
            : showAppBar
                ? PreferredSize(
                    preferredSize: Size.fromHeight(
                      AppThemes.dimens.appBarHeight,
                    ),
                    child: ChatifyAppBar(
                      isSliver: false,
                      title: title,
                      centerTitle: centerTitle,
                      transparent: appBarTransparent,
                      showBackButton: appBarShowBackButton,
                      actions: actions,
                      isPrimaryColor: isPrimaryColor,
                      bottom: bottom,
                      onPop: onWillPop,
                      backButtonAlwaysEnabled: backButtonAlwaysEnabled,
                    ),
                  )
                : null,
        bottomNavigationBar: bottomNavigationBar,
        body: SafeArea(
          child: _ConditionalRefreshIndicator(
            notificationPredicate: notificationPredicate,
            onRefresh: onRefresh,
            isNestedScrollEnabled: isNestedScrollEnabled,
            usePageView: usePageView,
            child: _ScreenWithAppBarBody(
              title: title,
              centerTitle: centerTitle,
              body: body,
              customPadding: customPadding,
              appBarShowBackButton: appBarShowBackButton,
              isHome: isHome,
              showAppBar: showAppBar,
              appBarTransparent: appBarTransparent,
              actions: actions,
              isPrimaryColor: isPrimaryColor,
              isNestedScrollEnabled: isNestedScrollEnabled,
              isBodyScrollable: isBodyScrollable,
              physics: physics,
              usePageView: usePageView,
              pageViewController: pageViewController,
              pageViewChildren: pageViewChildren,
              scrollController: scrollController,
              bottom: bottom,
              backButtonAlwaysEnabled: backButtonAlwaysEnabled,
              onPop: onWillPop,
            ),
          ),
        ),
      ),
    );
  }
}

The body of the widget above:

class _ScreenWithAppBarBody extends StatelessWidget {
  const _ScreenWithAppBarBody({
    required this.body,
    required this.appBarTransparent,
    required this.appBarShowBackButton,
    required this.isHome,
    required this.showAppBar,
    required this.physics,
    required this.customPadding,
    required this.centerTitle,
    required this.isPrimaryColor,
    required this.isNestedScrollEnabled,
    required this.isBodyScrollable,
    required this.usePageView,
    required this.title,
    required this.actions,
    required this.pageViewController,
    required this.pageViewChildren,
    required this.scrollController,
    required this.bottom,
    required this.backButtonAlwaysEnabled,
    required this.onPop,
  });

  final Widget? body;
  final bool appBarTransparent;
  final bool appBarShowBackButton;
  final bool isHome;
  final bool showAppBar;
  final ScrollPhysics physics;
  final double customPadding;
  final bool centerTitle;
  final bool isPrimaryColor;
  final bool isNestedScrollEnabled;
  final bool isBodyScrollable;
  final bool usePageView;
  final bool backButtonAlwaysEnabled;

  final String? title;
  final List<Widget>? actions;
  final PageController? pageViewController;
  final List<Widget>? pageViewChildren;
  final ScrollController? scrollController;
  final PreferredSizeWidget? bottom;
  final Future<bool> Function()? onPop;

  @override
  Widget build(BuildContext context) {
    return _ConditionalNestedScrollView(
      isNestedScrollEnabled: isNestedScrollEnabled && isBodyScrollable,
      isHome: isHome,
      showAppBar: showAppBar,
      title: title,
      centerTitle: centerTitle,
      appBarTransparent: appBarTransparent,
      appBarShowBackButton: appBarShowBackButton,
      actions: actions,
      isPrimaryColor: isPrimaryColor,
      scrollController: scrollController,
      bottom: bottom,
      backButtonAlwaysEnabled: backButtonAlwaysEnabled,
      onPop: onPop,
      body: _ConditionalPageView(
        isNestedScrollEnabled: isNestedScrollEnabled,
        isBodyScrollable: isBodyScrollable,
        physics: physics,
        customPadding: customPadding,
        body: body,
        usePageView: usePageView,
        pageViewController: pageViewController,
        pageViewChildren: pageViewChildren,
        scrollController: scrollController,
      ),
    );
  }
}

NestedScrollView inside of the body above:

class _ConditionalNestedScrollView extends StatelessWidget {
  const _ConditionalNestedScrollView({
    required this.body,
    required this.appBarTransparent,
    required this.appBarShowBackButton,
    required this.isHome,
    required this.showAppBar,
    required this.centerTitle,
    required this.isPrimaryColor,
    required this.isNestedScrollEnabled,
    required this.title,
    required this.actions,
    required this.scrollController,
    required this.bottom,
    required this.backButtonAlwaysEnabled,
    required this.onPop,
  });

  final Widget body;
  final bool appBarTransparent;
  final bool appBarShowBackButton;
  final bool isHome;
  final bool showAppBar;
  final bool centerTitle;
  final bool isPrimaryColor;
  final bool isNestedScrollEnabled;
  final bool backButtonAlwaysEnabled;

  final String? title;
  final List<Widget>? actions;
  final ScrollController? scrollController;
  final PreferredSizeWidget? bottom;
  final Future<bool> Function()? onPop;

  @override
  Widget build(BuildContext context) {
    if (isNestedScrollEnabled) {
      return NestedScrollView(
        floatHeaderSlivers: true,
        controller: scrollController,
        headerSliverBuilder: (_, __) => <Widget>[
          if (isHome && showAppBar)
            ChatifyAppBar(
              title: title ?? context.tr.app_name,
              centerTitle: centerTitle,
              transparent: appBarTransparent,
              showBackButton: false,
              actions: actions,
              isPrimaryColor: isPrimaryColor,
              bottom: bottom,
            )
          else if (showAppBar)
            ChatifyAppBar(
              title: title,
              centerTitle: centerTitle,
              transparent: appBarTransparent,
              showBackButton: appBarShowBackButton,
              actions: actions,
              isPrimaryColor: isPrimaryColor,
              bottom: bottom,
              backButtonAlwaysEnabled: backButtonAlwaysEnabled,
              onPop: onPop,
            ),
        ],
        body: body,
      );
    } else {
      return body;
    }
  }
}

PageView inside of the body above:

class _ConditionalPageView extends StatelessWidget {
  const _ConditionalPageView({
    required this.body,
    required this.customPadding,
    required this.physics,
    required this.isNestedScrollEnabled,
    required this.isBodyScrollable,
    required this.usePageView,
    required this.pageViewController,
    required this.pageViewChildren,
    required this.scrollController,
  });

  final Widget? body;
  final double customPadding;
  final ScrollPhysics physics;
  final bool isNestedScrollEnabled;
  final bool isBodyScrollable;
  final bool usePageView;

  final PageController? pageViewController;
  final List<Widget>? pageViewChildren;
  final ScrollController? scrollController;

  @override
  Widget build(BuildContext context) {
    if (usePageView) {
      return PageView.builder(
        controller: pageViewController,
        physics: const NeverScrollableScrollPhysics(),
        itemBuilder: (BuildContext context, int index) {
          return _SingleChildScrollView(
            scrollController: scrollController,
            physics: physics,
            isNestedScrollEnabled: isNestedScrollEnabled,
            isBodyScrollable: isBodyScrollable,
            customPadding: customPadding,
            body: pageViewChildren![index],
          );
        },
      );
    } else {
      return _SingleChildScrollView(
        scrollController: scrollController,
        physics: physics,
        isNestedScrollEnabled: isNestedScrollEnabled,
        isBodyScrollable: isBodyScrollable,
        customPadding: customPadding,
        body: body!,
      );
    }
  }
}

Inside of the widget above – used to display child of the ScreenWithAppBar:

class _SingleChildScrollView extends StatelessWidget {
  const _SingleChildScrollView({
    required this.body,
    required this.customPadding,
    required this.physics,
    required this.isNestedScrollEnabled,
    required this.isBodyScrollable,
    required this.scrollController,
  });

  final Widget body;
  final double customPadding;
  final ScrollPhysics physics;
  final bool isNestedScrollEnabled;
  final bool isBodyScrollable;
  final ScrollController? scrollController;

  @override
  Widget build(BuildContext context) {
    if (isBodyScrollable) {
      return SingleChildScrollView(
        controller: isNestedScrollEnabled ? null : scrollController,
        physics: physics,
        child: _Body(
          customPadding: customPadding,
          body: body,
        ),
      );
    } else {
      return _Body(
        customPadding: customPadding,
        body: body,
      );
    }
  }
}

Custom body with shimmer of the widget above:

class _Body extends StatelessWidget {
  const _Body({
    required this.body,
    required this.customPadding,
  });

  final Widget body;
  final double customPadding;

  @override
  Widget build(BuildContext context) {
    return ScreenPadding(
      customPadding: customPadding,
      child: Shimmer(
        child: body,
      ),
    );
  }
}

Sorry for such a lengthy message and code, I am in a pickle and don’t really know how to go from here. Any help would be much appreciated!

2

Answers


  1. try this

    SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
            if (scrollController.hasClients) {
              scrollController.animateTo(
               scrollController.positions.last.maxScrollExtent + anyIndex,/// Let's say + 50
                duration: 600.milliseconds,
                curve: Curves.ease,
              );
            }
          });
    
    Login or Signup to reply.
  2. There are a couple things I would try to get the scroll animation working properly in your nested ScrollView setup:

    1. Move the scroll animation logic into the ChatScreen widget itself, rather than the parent ScreenWithAppBar. This will allow you to directly control the scrolling of the inner _MessageList ListView.
    @override 
    void initState() {
      ...
    
      SchedulerBinding.instance.addPostFrameCallback((_) {
        Scrollable.ensureVisible(
          context, 
          duration: Duration(milliseconds: 300),
          curve: Curves.easeOut,
          alignment: 1.0, 
        );
      });
    }
    
    1. Simplify the scroll view nesting – the multiple layers of SingleChildScrollView, PageView, etc may be interfering with the scroll animation. Try removing unused scroll widgets.

    2. Wrap the _MessageList in a NotificationListener to listen for scroll notifications and programmatically scroll when needed.

    3. Use a ScrollController specifically on the _MessageList rather than the parent widgets.

    4. Call jumpTo() or animateTo() on the _MessageList scroll controller once layout is complete.

    Essentially moving the scroll logic closer to the ListView itself rather than ancestor widgets should help resolve the issue.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search