flutter_slidable icon indicating copy to clipboard operation
flutter_slidable copied to clipboard

How to close a Slidable from outside its widget tree?

Open chrisalex-w opened this issue 3 years ago • 8 comments

I have a ListView and each one of its items is wrapped with a Slidable. These tiles are composed by a TextFormField. I would like to be able to close a Slidable by tapping another tile. To be more precise, by tapping the TextFormField of another tile.

There are three tiles with Slidables attached to them.

In the following images, from left to right:

  1. I slide the second tile.
  2. I tap the TextFormField of the third tile.
  3. Then, the Slidable of the second tile should be closed.

1

Simple App:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
            elevation: 0,
            title: const Text('Slidable from outside'),
        ),
        body: SlidableAutoCloseBehavior(
          closeWhenOpened: true,
          closeWhenTapped: false,
          child: ListView.builder(
            itemCount: 3,
            itemBuilder: (context, index) {
              return const MyTile();
            },
          ),
        ),
      ),
    );
  }
}

Tile:

class MyTile extends StatelessWidget {
  const MyTile({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Slidable(
      closeOnScroll: false,
      startActionPane: const ActionPane(
        dragDismissible: false,
        motion: ScrollMotion(),
        children: [
          SlidableAction(
            backgroundColor: Color(0xFFe0e0e0),
            icon: Icons.remove_circle_outline_outlined,
            autoClose: false,
            onPressed: null,
          ),
          SlidableAction(
            backgroundColor: Color(0xFFe0e0e0),
            icon: Icons.add_circle_outline_outlined,
            autoClose: false,
            onPressed: null,
          ),
        ],
      ),
      child: Container(
        padding: const EdgeInsets.all(24),
        child: TextFormField(
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.w600,
            color: Colors.grey[800],
          ),
          decoration: const InputDecoration(
            isDense: true,
            border: InputBorder.none,
            contentPadding: EdgeInsets.zero,
          ),
          initialValue: '25.000',
          onTap: () {
            //Some code that triggers the close action of another Slidable
          },
        ),
      ),
    );
  }
}

From what I understand, in old versions of this package you used a SlidableController, but it has changed now. A recommended way is to wrap the list with a SlidableAutoCloseBehavior, but it can't control each Slidable independently.

The parameter closeWhenTapped is the closest to a solution because if I set this to true, it let me close the tile after tapping in another tile, but, I have to tap twice, hence the TextFormField is not selectable at first touch. So I set it to false in order to let me select the TextFormField although without being able to close the Slidable automatically.

chrisalex-w avatar Mar 23 '22 16:03 chrisalex-w

@kyuberi Do yo have any solution for workaround?

voratham avatar Apr 06 '22 06:04 voratham

@kyuberi Have you tried static method Slidable.of(BuildContext context) which returns SlidableController?

feduke-nukem avatar Aug 29 '22 14:08 feduke-nukem

@chrisalex-w have you managed to find a solution for that? I'm having a similar issue. It used to be that with an opened slidable you could tap anywhere and it would close, now it is not the case. I tried to wrap my entire app in the SlidableAutoCloseBehavior but it only works if another slidable in the same group is tapped. @letsar any idea how to get that behaviour back?

hicnar avatar Apr 13 '23 10:04 hicnar

@letsar Do you have any solution for this problem?

binhdi0111 avatar Feb 16 '24 07:02 binhdi0111

Hi @chrisalex-w, you can get a SlidableController from a child of the Slidable. With that you can, for example, use it when a widget loses its focus.

letsar avatar Feb 17 '24 10:02 letsar

Actually, you may try something like this:

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SlidableOwner(
        child: Scaffold(
          appBar: AppBar(
            elevation: 0,
            title: const Text('Slidable from outside'),
          ),
          body: SlidableAutoCloseBehavior(
            closeWhenOpened: true,
            closeWhenTapped: false,
            child: ListView.builder(
              itemCount: 3,
              itemBuilder: (context, index) {
                return MyTile(index: index);
              },
            ),
          ),
        ),
      ),
    );
  }
}

class MyTile extends StatelessWidget {
  final int index;

  const MyTile({
    super.key,
    required this.index,
  });

  @override
  Widget build(BuildContext context) {
    return Slidable(
      closeOnScroll: false,
      startActionPane: const ActionPane(
        dragDismissible: false,
        motion: ScrollMotion(),
        children: [
          SlidableAction(
            backgroundColor: Color(0xFFe0e0e0),
            icon: Icons.remove_circle_outline_outlined,
            autoClose: false,
            onPressed: null,
          ),
          SlidableAction(
            backgroundColor: Color(0xFFe0e0e0),
            icon: Icons.add_circle_outline_outlined,
            autoClose: false,
            onPressed: null,
          ),
        ],
      ),
      child: SlidableOwnerTarget(
        id: index,
        child: Container(
          padding: const EdgeInsets.all(24),
          child: TextFormField(
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.w600,
              color: Colors.grey[800],
            ),
            decoration: const InputDecoration(
              isDense: true,
              border: InputBorder.none,
              contentPadding: EdgeInsets.zero,
            ),
            initialValue: '25.000',
            // Close specific registered Slidable
            onTap: () => SlidableOwner.of(context).close(index + 1),
          ),
        ),
      ),
    );
  }
}

class SlidableOwnerScope extends InheritedWidget {
  final SlidableOwnerState state;

  const SlidableOwnerScope({
    super.key,
    required super.child,
    required this.state,
  });

  @override
  bool updateShouldNotify(SlidableOwnerScope oldWidget) {
    return false;
  }
}

class SlidableOwner extends StatefulWidget {
  final Widget child;

  const SlidableOwner({
    super.key,
    required this.child,
  });

  @override
  State<SlidableOwner> createState() => SlidableOwnerState();

  static SlidableOwnerState of(BuildContext context) {
    return context.getInheritedWidgetOfExactType<SlidableOwnerScope>()!.state;
  }
}

class SlidableOwnerState extends State<SlidableOwner> {
  final _controllers = <Object, SlidableController>{};

  @override
  Widget build(BuildContext context) {
    return SlidableOwnerScope(
      state: this,
      child: widget.child,
    );
  }

  Future<void> close(Object id) async {
    final controller = _controllers[id];

    if (controller == null) return;

    return controller.close();
  }

  Future<void> closeAll() async =>
      await Future.wait(_controllers.values.map((e) => e.close()).toList());
}

class SlidableOwnerTarget extends StatefulWidget {
  final Widget child;
  final Object id;

  const SlidableOwnerTarget({
    super.key,
    required this.child,
    required this.id,
  });

  @override
  State<SlidableOwnerTarget> createState() => _SlidableOwnerTargetState();
}

class _SlidableOwnerTargetState extends State<SlidableOwnerTarget> {
  late SlidableOwnerState _slidableOwnerState;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    _slidableOwnerState = SlidableOwner.of(context);

    _slidableOwnerState._controllers[widget.id] = Slidable.of(context)!;
  }

  @override
  void dispose() {
    super.dispose();
    _slidableOwnerState._controllers.remove(widget.id);
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

Result: Simulator Screen Recording - iPhone 11 Pro - 2024-02-18 at 16 12 04

feduke-nukem avatar Feb 18 '24 13:02 feduke-nukem