Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
chrisalex-w opened this issue Mar 23, 2022 · 7 comments
Open

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

chrisalex-w opened this issue Mar 23, 2022 · 7 comments

Comments

@chrisalex-w
Copy link

chrisalex-w commented Mar 23, 2022

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
Copy link

voratham commented Apr 6, 2022

@kyuberi Do yo have any solution for workaround?

@feduke-nukem
Copy link

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

@hicnar
Copy link

hicnar commented Apr 13, 2023

@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
Copy link

@letsar Do you have any solution for this problem?

@letsar
Copy link
Owner

letsar commented Feb 17, 2024

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
Copy link

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
Copy link

khomin commented Sep 26, 2024

doesn't work with AutomaticKeepAliveClientMixin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants