Making a DraggableScrollableSheet scroll up or down using GetX (Flutter)

Introduction to DraggableScrollableSheet

In many mobile applications, you might have seen an area at the bottom of the screen which can be dragged up to reveal more information or dragged down to hide information. One such example is that of Google Maps.

The sheet at the bottom in Google Maps can be dragged up to reveal more information, and dragged down to hide information.

To implement a sheet like this in Flutter, we use the DraggableScrollableSheet widget.

Example Implementation from the Flutter Documentation

The Flutter documentation gives us code to implement such a sheet. The code snippet below is from the official Flutter documentation, with a slight change. The thing to note is that the code snippet provides us with a Grabber widget too, which is the small notch we see at the top of the DraggableScrollableSheet. The sheet does not have this by default, the programmer has to create it.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

/// Flutter code sample for [DraggableScrollableSheet].

void main() => runApp(const DraggableScrollableSheetExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade100),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('DraggableScrollableSheet Sample'),
        ),
        body: const DraggableScrollableSheetExample(),
      ),
    );
  }
}

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

  @override
  State<DraggableScrollableSheetExample> createState() =>
      _DraggableScrollableSheetExampleState();
}

class _DraggableScrollableSheetExampleState
    extends State<DraggableScrollableSheetExample> {
  double _sheetPosition = 0.5;
  final double _dragSensitivity = 600;

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return DraggableScrollableSheet(
      initialChildSize: _sheetPosition,
      builder: (BuildContext context, ScrollController scrollController) {
        return ColoredBox(
          color: colorScheme.primary,
          child: Column(
            children: <Widget>[
              Grabber(
                onVerticalDragUpdate: (DragUpdateDetails details) {
                  setState(() {
                    _sheetPosition -= details.delta.dy / _dragSensitivity;
                    if (_sheetPosition < 0.25) {
                      _sheetPosition = 0.25;
                    }
                    if (_sheetPosition > 1.0) {
                      _sheetPosition = 1.0;
                    }
                  });
                },
                isOnDesktopAndWebAndAndroid: _isOnDesktopAndWebAndAndroid,
              ),
              Flexible(
                child: ListView.builder(
                  controller: _isOnDesktopAndWebAndAndroid ? null : scrollController,
                  itemCount: 25,
                  itemBuilder: (BuildContext context, int index) {
                    return ListTile(
                      title: Text(
                        'Item $index',
                        style: TextStyle(color: colorScheme.surface),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  bool get _isOnDesktopAndWebAndAndroid {
    if (kIsWeb) {
      return true;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
      case TargetPlatform.android:
        return true;

      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
        return false;
    }
  }
}

/// A draggable widget that accepts vertical drag gestures
/// and this is only visible on desktop and web platforms.
class Grabber extends StatelessWidget {
  const Grabber({
    super.key,
    required this.onVerticalDragUpdate,
    required this.isOnDesktopAndWebAndAndroid,
  });

  final ValueChanged<DragUpdateDetails> onVerticalDragUpdate;
  final bool isOnDesktopAndWebAndAndroid;

  @override
  Widget build(BuildContext context) {
    if (!isOnDesktopAndWebAndAndroid) {
      return const SizedBox.shrink();
    }
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return GestureDetector(
      onVerticalDragUpdate: onVerticalDragUpdate,
      child: Container(
        width: double.infinity,
        color: colorScheme.onSurface,
        child: Align(
          alignment: Alignment.topCenter,
          child: Container(
            margin: const EdgeInsets.symmetric(vertical: 8.0),
            width: 32.0,
            height: 4.0,
            decoration: BoxDecoration(
              color: colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(8.0),
            ),
          ),
        ),
      ),
    );
  }
}

The Grabber widget takes two arguments: onVerticalDragUpdate, which changes the height of the sheet upon dragging it up or down, and isOnDesktopAndWebAndAndroid, which shows the grabber only if we are on the web, on desktop or on Android. We get the following output on running this code in Android:

The problem with this implementation

While this implementation does achieve the desired functionality, it has a problem: it is using setState() to rebuild the UI. Each time setState() is called, the entire UI rebuilds instead of the specific widget we want to rebuild. While this is not a problem for simple UIs with few widgets, this becomes expensive when we have a complex UI made up of many widgets.

Enter GetX

The solution to the above problem is to use state management. State management ensures only the specific widget we want to rebuild is getting rebuilt, instead of the entire UI. For this article, we will use GetX.

Rewriting the above code using GetX

The first step is to install the GetX package using the following command:
flutter pub add get

Then we import this package in our main.dart file:

import 'package:get/get.dart';

Now we will create a class in our main.dart file by the name ScrollSheetController:

class ScrollSheetController extends GetxController{

}

In this class, we declare the following variables and a function:

class ScrollSheetController extends GetxController{
 RxDouble dragSensitivity = 600.0.obs; //How smoothly the sheet will get dragged. Higher means smoother, lower means less smooth
 RxDouble sheetPosition = 0.5.obs; //The height of the sheet

 //The GetX equivalent of the setState() function
 void onVerticalDragUpdate(){

 }

}

Now let’s start writing the function.

//The GetX equivalent of the setState() function
 void onVerticalDragUpdate(details){
  sheetPosition.value -= details.delta.dy / dragSensitivity.value;
  if (sheetPosition.value < 0.25) {
    sheetPosition.value = 0.25;
  }
  if (sheetPosition.value > 1.0) {
    sheetPosition.value = 1.0;
  }
 }

Using the function in our DraggableScrollableSheet

The first step is to declare a controller object as below:

class _DraggableScrollableSheetExampleState
    extends State<DraggableScrollableSheetExample> {


  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    //Add the line below to your code.
    late ScrollSheetController scrollSheetController = Get.put(ScrollSheetController());

Now we have to use this object in our DraggableScrollableSheet widget. This will be done as below:

return DraggableScrollableSheet(
      initialChildSize: scrollSheetController.sheetPosition.value, //Change this line to use the height value inside the controller.
      builder: (BuildContext context, ScrollController scrollController) {
        return ColoredBox(
          color: colorScheme.primary,
          child: Column(
            children: <Widget>[
              Grabber(
                //Call the function inside the GetX class in here.
                onVerticalDragUpdate: (DragUpdateDetails details) {
                  scrollSheetController.onVerticalDragUpdate(details);
                },
                isOnDesktopAndWebAndAndroid: _isOnDesktopAndWebAndAndroid,
              ),

Now, for the last and most rewarding step, reflecting the changes in the UI. For this we first need to wrap our DraggableScrollableSheet in an Obx widget.

//Wrapping the DraggableScrollableSheet in the Obx widget
    return Obx(
      ()=> DraggableScrollableSheet(
        initialChildSize: scrollSheetController.sheetPosition.value, //Change this line to use the height value inside the controller.
        builder: (BuildContext context, ScrollController scrollController) {
          return ColoredBox(
            color: colorScheme.primary,
            child: Column(
              children: <Widget>[
                Grabber(
                  //Call the function inside the GetX class in here.
                  onVerticalDragUpdate: (DragUpdateDetails details) {
                    scrollSheetController.onVerticalDragUpdate(details);
                  },
                  isOnDesktopAndWebAndAndroid: _isOnDesktopAndWebAndAndroid,
                ),

Now, our UI works exactly the same, but only the DraggableScrollableSheet is rebuilt each time. That is the power of GetX.

Here is how the whole code looks like at the end in main.dart:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

/// Flutter code sample for [DraggableScrollableSheet].

void main() => runApp(const DraggableScrollableSheetExampleApp());


class ScrollSheetController extends GetxController{
 RxDouble dragSensitivity = 600.0.obs; //How smoothly the sheet will get dragged. Higher means smoother, lower means less smooth
 RxDouble sheetPosition = 0.5.obs; //The height of the sheet

 //The GetX equivalent of the setState() function
 void onVerticalDragUpdate(details){
  sheetPosition.value -= details.delta.dy / dragSensitivity.value;
  if (sheetPosition.value < 0.25) {
    sheetPosition.value = 0.25;
  }
  if (sheetPosition.value > 1.0) {
    sheetPosition.value = 1.0;
  }
 }

}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade100),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('DraggableScrollableSheet Sample'),
        ),
        body: const DraggableScrollableSheetExample(),
      ),
    );
  }
}

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

  @override
  State<DraggableScrollableSheetExample> createState() =>
      _DraggableScrollableSheetExampleState();
}

class _DraggableScrollableSheetExampleState
    extends State<DraggableScrollableSheetExample> {


  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    late ScrollSheetController scrollSheetController = Get.put(ScrollSheetController());

    //Wrapping the DraggableScrollableSheet in the Obx widget
    return Obx(
      ()=> DraggableScrollableSheet(
        initialChildSize: scrollSheetController.sheetPosition.value, //Change this line to use the height value inside the controller.
        builder: (BuildContext context, ScrollController scrollController) {
          return ColoredBox(
            color: colorScheme.primary,
            child: Column(
              children: <Widget>[
                Grabber(
                  //Call the function inside the GetX class in here.
                  onVerticalDragUpdate: (DragUpdateDetails details) {
                    scrollSheetController.onVerticalDragUpdate(details);
                  },
                  isOnDesktopAndWebAndAndroid: _isOnDesktopAndWebAndAndroid,
                ),
                Flexible(
                  child: ListView.builder(
                    controller: _isOnDesktopAndWebAndAndroid ? null : scrollController,
                    itemCount: 25,
                    itemBuilder: (BuildContext context, int index) {
                      return ListTile(
                        title: Text(
                          'Item $index',
                          style: TextStyle(color: colorScheme.surface),
                        ),
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

  bool get _isOnDesktopAndWebAndAndroid {
    if (kIsWeb) {
      return true;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
      case TargetPlatform.android:
        return true;

      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
        return false;
    }
  }
}

/// A draggable widget that accepts vertical drag gestures
/// and this is only visible on desktop and web platforms.
class Grabber extends StatelessWidget {
  const Grabber({
    super.key,
    required this.onVerticalDragUpdate,
    required this.isOnDesktopAndWebAndAndroid,
  });

  final ValueChanged<DragUpdateDetails> onVerticalDragUpdate;
  final bool isOnDesktopAndWebAndAndroid;

  @override
  Widget build(BuildContext context) {
    if (!isOnDesktopAndWebAndAndroid) {
      return const SizedBox.shrink();
    }
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return GestureDetector(
      onVerticalDragUpdate: onVerticalDragUpdate,
      child: Container(
        width: double.infinity,
        color: colorScheme.onSurface,
        child: Align(
          alignment: Alignment.topCenter,
          child: Container(
            margin: const EdgeInsets.symmetric(vertical: 8.0),
            width: 32.0,
            height: 4.0,
            decoration: BoxDecoration(
              color: colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(8.0),
            ),
          ),
        ),
      ),
    );
  }
}

And that wraps up how to control a DraggableScrollableSheet using GetX.

Conclusion

We have learned today how to control a DraggableScrollableSheet using a controller in GetX. However, there is still a problem with this code: we cannot interact with the background. We will be taking a look at how to solve this problem in the next article. If you found this article helpful, please like and share, it encourages me to write more articles like these. Until next time.

0
Subscribe to my newsletter

Read articles from Muhammad Taimoor directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Muhammad Taimoor
Muhammad Taimoor