top of page
shubhangisingh453

Exploring State Management and Gestures for Seamless App Development

Updated: Mar 13, 2023



State Management

State management in Flutter refers to the management of data and state within a Flutter application. State refers to the data that changes over time, such as the current user input, the current page being viewed, or the current state of an animation. Managing state is important because it allows Flutter applications to respond to user input and other events, and update the UI accordingly.

There are several ways to manage state in Flutter, including:

StatefulWidget and setState():


In Flutter, widgets are immutable, meaning that once they are created, they cannot be changed. However, we can use a StatefulWidget to manage the state of a widget. The StatefulWidget has a State object associated with it, which holds the widget's state data. When the widget needs to update its state, we can call setState() to trigger a rebuild of the widget with the new state.


How to update State and Widgets in App?


In an interactive and dynamic app, you may need to update the information and UI displayed in the app according to different conditions. In background, the states, values of variables may get changes but to update widgets setState() method is used. To show how to update the state and the widgets inside app, see the example below. In the example code below, we have a floating action button and one centered text with counter value. When user presses the floating action button, we will increase the counter value and update the displayed value on widget using setState().


How to Create Stateful Widget ?


To create the stateful widget, you need to extend the widget build class with "StatefulWidget" like below.


class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.red,
        title: Text('Coding age \nstateful widget'),
        leading:ClipOval(
          child: Image.network(
            'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR4NNNQD8Hsj0EywGnRRXhVsasaOVfXAVvxXM-FxQZnLA&s',
            width: 100,
            height: 100,
          ),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
        backgroundColor: Colors.red,
      ),
    );
  }
}




States changes from 0 to 1 after click on floating button.


In this example, the MyWidget class is a StatefulWidget that manages the state of the _counter variable. When the user presses the floatingActionButton, the _incrementCounter() function is called, which calls setState() to update the _counter variable and trigger a rebuild of the widget.

Provider package:


Provider is a Flutter package that provides a way to share data between widgets in an efficient and flexible way. Provider works by creating a "provider" object that holds the state data, and then "providing" that object to any widget that needs it. When the state data changes, all widgets that depend on that data are automatically rebuilt.


Example: Let us build above application using provider.


class MyCounter with ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyCounter(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('My Widget'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Consumer<MyCounter>(
                builder: (context, counter, child) => Text(
                  'You have pushed the button this many times:',
                ),
              ),
              Consumer<MyCounter>(
                builder: (context, counter, child) => Text(
                  '${counter.counter}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Provider.of<MyCounter>(context, listen: false).increment();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

The MyWidget class uses the ChangeNotifierProvider to create the provider object and provide it to its child widgets. The MyCounter class has a _counter variable that is updated when the user presses the FloatingActionButton, and notifyListeners() is called to notify all dependent widgets that the data has changed. The two Text widgets in the build() method are dependent on the MyCounter data, so they are wrapped in a Consumer widget. The Consumer widget automatically rebuilds its child widgets whenever the data it depends on changes. Finally, the FloatingActionButton onPressed() method uses Provider.of to get the MyCounter object and call its increment() method.

BLoC (Business Logic Component) pattern:


BLoC is a design pattern for managing state and data flow in Flutter applications. The BLoC pattern separates the UI components from the business logic, making it easier to manage the application's state and making the code more reusable.

Example:

Same example built using BLoC


class CounterBloc {
  int _counter = 0;
  final _counterController = StreamController<int>();

  Stream<int> get counterStream => _counterController.stream;

  void incrementCounter() {
    _counter++;
    _counterController.sink.add(_counter);
  }

  void dispose() {
    _counterController.close();
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final CounterBloc _bloc = CounterBloc();

    return Scaffold(
      appBar: AppBar(
        title: Text('My Widget'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            StreamBuilder<int>(
              stream: _bloc.counterStream,
              initialData: 0,
              builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                return Text(
                  'You have pushed the button this many times:',
                );
              },
            ),
            StreamBuilder<int>(
              stream: _bloc.counterStream,
              initialData: 0,
              builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                return Text(
                  '${snapshot.data}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _bloc.incrementCounter();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


In this example, the CounterBloc class is responsible for managing the state data and business logic of the widget. It has a _counter variable and a StreamController to hold and update the data. The MyWidget class creates an instance of the CounterBloc and uses StreamBuilder widgets to subscribe to the stream of data. The StreamBuilder widget automatically rebuilds its child widgets whenever the data it depends on changes. Finally, the FloatingActionButton onPressed() method calls the CounterBloc's incrementCounter() method to update the state data. There are many other approaches and patterns, and the choice of state management depends on the complexity and requirements of the application.


Inherited Widget



An InheritedWidget is a widget that passes down its state to its children. This allows you to share state between widgets without having to pass it down explicitly. This is particularly useful for managing app-level state that needs to be shared between many different widgets.


// Define the state that will be shared between the widgets
class CounterState {
  int count = 0;
}

// Create an InheritedWidget to share the state between the widgets
class CounterProvider extends InheritedWidget {
  final CounterState state;

  CounterProvider({Key? key, required this.state, required Widget child})
      : super(key: key, child: child);

  // Define a convenience method to get the CounterProvider from the widget tree
  static CounterProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterProvider>();
  }

  // This method is called when the state changes and the widget needs to be rebuilt
  @override
  bool updateShouldNotify(CounterProvider old) => true;
}

// Widget that displays the current count
class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get the CounterState from the CounterProvider
    final state = CounterProvider.of(context)!.state;

    return Text('Count: ${state.count}');
  }
}

// Widget that increments the count when pressed
class CounterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get the CounterState from the CounterProvider
    final state = CounterProvider.of(context)!.state;

    return ElevatedButton(
      onPressed: () {
        // Update the state by incrementing the count
        state.count++;
      },
      child: Text('Increment'),
    );
  }
}

// Widget that combines the CounterDisplay and CounterButton
class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Create a new CounterState to share between the widgets
    final state = CounterState();

    // Wrap the widgets in a CounterProvider to share the state
    return CounterProvider(
      state: state,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CounterDisplay(),
          CounterButton(),
        ],
      ),
    );
  }

In this example, we define a CounterState class to hold the state that we want to share between the widgets. We then create an InheritedWidget called CounterProvider that holds an instance of CounterState and a child widget. We also define a convenience method called of that we can use to get the CounterProvider from the widget tree.

We then define two widgets called CounterDisplay and CounterButton. CounterDisplay displays the current count, and CounterButton increments the count when pressed. Both widgets use the CounterProvider to get the CounterState.

Finally, we define a CounterApp widget that creates a new CounterState and wraps the CounterDisplay and CounterButton widgets in a CounterProvider. This allows the CounterDisplay and CounterButton to share the same instance of CounterState.


Gestures

Gestures are a way for users to interact with your app by tapping, swiping, dragging, or performing other touch-based actions on the screen. In Flutter, you can detect these gestures and respond to them using gesture recognizers.

There are several types of gesture recognizers in Flutter, each designed to detect a specific type of gesture. Here are some examples:


GestureDetector


The GestureDetector widget is used to detect a variety of gestures, including taps, double taps, long presses, and drags. To use it, you wrap a child widget with a GestureDetector widget and specify the type of gesture you want to detect using the appropriate callback.



GestureDetector(   
         onTap: () {     // Handle tap gesture   
              },   
         onDoubleTap: () {     // Handle double tap gesture   
},   
         onLongPress: () {     // Handle long press gesture   },                       

         onVerticalDragUpdate: (DragUpdateDetails details) { 
    // Handle vertical drag gesture   
},   
    child: Container(
     width: 200,
         height: 200,  
        color: Colors.blue,
            ), 
        )

In this example, we're using the GestureDetector widget to detect a variety of gestures, including taps, double taps, long presses, and vertical drags. When a gesture is detected, the appropriate callback is called.


Tap Gesture

A tap gesture is a quick touch down and release of the finger. It is recognized when the user touches the screen and immediately removes their finger without any movement. A typical use case of this gesture is to trigger an action when the user taps on a button or an icon.

Example:


GestureDetector(
  onTap: () {
    // handle tap gesture
  },
  child: Container(
    child: Text('Tap me'),
  ),
);

Long Press Gesture

A long press gesture is recognized when the user presses and holds down their finger on the screen for a certain duration of time. This gesture is typically used to trigger a context menu or initiate a drag operation.

Example:


GestureDetector(
  onLongPress: () {
    // handle long press gesture
  },
  child: Container(
    child: Text('Long press me'),
  ),
);

Scale Gesture

A scale gesture is recognized when the user performs a pinch or zoom gesture on the screen. This gesture is typically used to zoom in or out on an image or a map.

Example:


GestureDetector(
  onScaleUpdate: (details) {
    setState(() {
      // update scale factor of image being zoomed
      scale += details.scale - 1;
    });
  },
  child: Container(
    child: Image.network('https://picsum.photos/200'),
  ),
);

Swipe Gesture

A swipe gesture is recognized when the user performs a quick movement on the screen in a specific direction. This gesture is typically used to navigate between screens or dismiss a dialog.

Example:


GestureDetector(
  onHorizontalDragEnd: (details) {
    if (details.velocity.pixelsPerSecond.dx > 0) {
      // handle swipe to right gesture
    } else {
      // handle swipe to left gesture
    }
  },
  child: Container(
    child: Text('Swipe me'),
  ),
);

These are just a few examples of the types of gestures that can be recognized in Flutter. By using GestureDetector widget, developers can handle these gestures and perform specific actions as per their needs.


Draggable


The Draggable widget is used to detect and respond to drag gestures. To use it, you wrap a child widget with a Draggable widget and specify a DragTarget widget to receive the drag data.


Draggable(
  data: "Hello World!",
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
  feedback: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    child: Text("Dragging..."),
  ),
  childWhenDragging: Container(
    width: 200,
    height: 200,
    color: Colors.grey,
  ),
)

DragTarget<String>(
  builder: (BuildContext context, List<String?> candidateData, List<dynamic> rejectedData) {
    return Container(
      width: 200,
      height: 200,
      color: Colors.green,
      child: Center(
        child: Text(candidateData.isNotEmpty ? candidateData.first! : "Drop Here!"),
      ),
    );
  },
  onWillAccept: (String? data) {
    return true;
  }
  onAccept: (String? data) {
    // Handle drag and drop here
  },
)

In this example, we're using the Draggable widget to detect drag gestures on a blue container. When the user starts dragging the container, the feedback widget is displayed (in this case, a red container with the text "Dragging..."). The childWhenDragging widget is displayed in place of the original child widget while the user is dragging.

We've also included a DragTarget widget to receive the drag data. In this case, we're accepting data of type String, and we've specified a builder callback to display a green container with the text "Drop Here!". When the user drops the dragged container onto the target, the onAccept callback is called.


Dismissible

The Dismissible widget is used to detect and respond to swipe gestures. To use it, you wrap a child widget with a Dismissible widget and specify a direction for the swipe gesture.


void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const MyStatefulWidget(),
      ),
    );
  }
}

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

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  List<int> items = List<int>.generate(100, (int index) => index);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      padding: const EdgeInsets.symmetric(vertical: 16),
      itemBuilder: (BuildContext context, int index) {
        return Dismissible(
          background: Container(
            color: Colors.green,
          ),
          key: ValueKey<int>(items[index]),
          onDismissed: (DismissDirection direction) {
            setState(() {
              items.removeAt(index);
            });
          },
          child: ListTile(
            title: Text(
              'Item ${items[index]}',
            ),
          ),
        );
      },
    );
  }}

Thanks for reading, and happy coding!


Mastering Flutter Development Series Article - 8 -> Navigation and Routing in Flutter: A Guide to Building Dynamic UIs




bottom of page