letsar / flutter_slidable

A Flutter implementation of slidable list item with directional slide actions.
MIT License
2.7k stars 582 forks source link

How to close a Slidable from outside its widget tree? #323

Open chrisalex-w opened 2 years ago

chrisalex-w commented 2 years ago

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.

voratham commented 2 years ago

@kyuberi Do yo have any solution for workaround?

feduke-nukem commented 2 years ago

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

hicnar commented 1 year ago

@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?

binhdi0111 commented 7 months ago

@letsar Do you have any solution for this problem?

letsar commented 7 months ago

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.

feduke-nukem commented 7 months ago

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

khomin commented 2 weeks ago

doesn't work with AutomaticKeepAliveClientMixin