Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter - select only single item in list view

In my app I am generating a ListView and items can be highlighted by tapping on them. That works fine and I also have a callback function that gives me the key for the just selected item. I can currently manually deselect the item by tapping on it again, but will ultimately take that functionality out.

My problem is that I want one and only one item to be selected at a time. In order to create the list I currently take some initial content in the form of a list, generate the tiles and add them to another list. I then use that list to create the ListView. My plan was on the callback from a new selection, run through the list of tiles and deselect them before highlighting the new chosen tile and carrying out the other functions. I have tried various methods to tell each tile to deselect itself but have not found any way to address each of the tiles. Currently I get the error:

Class 'OutlineTile' has no instance method 'deselect'. Receiver: Instance of 'OutlineTile' Tried calling: deselect()

I have tried to access a method within the tile class and to use a setter but neither worked so far. I am quite new to flutter so it could be something simple I am missing. My previous experience was with Actionscript where this system would have worked fine and I could access a method of an object (in this case the tile) easily as long s it is a public method.

I'd be happy to have another way to unselect the old item or to find a way to access a method within the tile. The challenge is to make the tiles show not highlighted without them being tapped themselves but when a different tile is tapped.

The code in my parent class is as follows:

class WorkingDraft extends StatefulWidget {
  final String startType;
  final String name;
  final String currentContent;
  final String currentID;
  final List startContent;

  WorkingDraft(
      {this.startType,
      this.name,
      this.currentContent,
      this.currentID,
      this.startContent});

  @override
  _WorkingDraftState createState() => _WorkingDraftState();
}

class _WorkingDraftState extends State<WorkingDraft> {
  final _formKey = GlobalKey<FormState>();
  final myController = TextEditingController();
  //String _startType;
  String _currentContent = "";
  String _name = "Draft";
  List _startContent = [];
  List _outLineTiles = [];
  int _counter = 0;

  @override
  void dispose() {
    // Clean up the controller when the widget is disposed.
    myController.dispose();
    super.dispose();
  }

  void initState() {
    super.initState();

    _currentContent = widget.currentContent;
    _name = widget.name;
    _startContent = widget.startContent;
    _counter = 0;

    _startContent.forEach((element) {
      _outLineTiles.add(OutlineTile(
        key: Key("myKey$_counter"),
        outlineName: element[0],
        myContent: element[1],
        onTileSelected: clearHilights,
      ));
      _counter++;
    });
  }

  dynamic clearHilights(Key myKey) {
    _outLineTiles.forEach((element) {
      element.deselect();  // this throws an error Class 'OutlineTile' has no instance method 'deselect'.
      Key _foundKey = element.key;
      print("Element Key $_foundKey");
    });
  }
.......

and further down within the widget build scaffold:

      child: ListView.builder(
        itemCount: _startContent.length,
        itemBuilder: (context, index) {
          return _outLineTiles[index];
        },
      ),

Then the tile class is as follows:

class OutlineTile extends StatefulWidget {
  final Key key;
  final String outlineName;
  final Icon myIcon;
  final String myContent;
  final Function(Key) onTileSelected;

  OutlineTile(
      {this.key,
      this.outlineName,
      this.myIcon,
      this.myContent,
      this.onTileSelected});

  @override
  _OutlineTileState createState() => _OutlineTileState();
}

class _OutlineTileState extends State<OutlineTile> {
  Color color;
  Key _myKey;

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

    color = Colors.transparent;
  }

  bool _isSelected = false;
  set isSelected(bool value) {
    _isSelected = value;
    print("set is selected to $_isSelected");
  }

  void changeSelection() {
    setState(() {
      _myKey = widget.key;
      _isSelected = !_isSelected;
      if (_isSelected) {
        color = Colors.lightBlueAccent;
      } else {
        color = Colors.transparent;
      }
    });
  }

  void deselect() {
    setState(() {
      isSelected = false;
      color = Colors.transparent;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 4.0),
      child: Row(
        children: [
          Card(
            elevation: 10,
            margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
            child: SizedBox(
              width: 180,
              child: Container(
                color: color,
                child: ListTile(
                  title: Text(widget.outlineName),
                  onTap: () {
                    if (widget.outlineName == "Heading") {
                      Text("Called Heading");
                    } else (widget.outlineName == "Paragraph") {
                      Text("Called Paragraph");
                    widget.onTileSelected(_myKey);
                    changeSelection();
                  },
                ),

         ........ 

Thanks for any help.

Amended Code sample and explanation, that builds to a complete project, from here:

Following the advice from phimath I have created a full buildable sample of the relevant part of my project.

The problem is that the tiles in my listview are more complex with several elements, many of which are buttons in their own right so whilst phimath's solution works for simple text tiles I have not been able to get it working inside my own project. My approach is trying to fundamentally do the same thing as phimath's but when I include these more complex tiles it fails to work.

This sample project is made up of three files. main.dart which simply calls the project and passes in some dummy data in the way my main project does. working_draft.dart which is the core of this issue. And outline_tile.dart which is the object that forms the tiles.

Within working draft I have a function that returns an updated list of the tiles which should show which tile is selected (and later any other changes from the other buttons). This gets called when first going to the screen. When the tile is tapped it uses a callback function to redraw the working_draft class but this seems to not redraw the list as I would expect it to. Any further guidance would be much appreciated.

The classes are:

first class is main.dart:

import 'package:flutter/material.dart';
import 'package:listexp/working_draft.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: WorkingDraft(
      startType: "Basic",
      name: "Draft",
      currentID: "anID",
      startContent: [
        ["Heading", "New Heading"],
        ["Paragraph", "New Text"],
        ["Image", "placeholder"],
        ["Signature", "placeholder"]
      ],
    ));
  }
}

Next file is working_draft.dart:

import 'package:flutter/material.dart';
import 'package:listexp/outline_tile.dart';

class WorkingDraft extends StatefulWidget {
  final String startType;
  final String name;
  final String currentContent;
  final String currentID;
  final List startContent;
  final int selectedIndex;

  WorkingDraft(
      {this.startType,
      this.name,
      this.currentContent,
      this.currentID,
      this.startContent,
      this.selectedIndex});

  @override
  _WorkingDraftState createState() => _WorkingDraftState();
}

class _WorkingDraftState extends State<WorkingDraft> {
  int selectedIndex;
  String _currentContent = "";
  String _name = "Draft";
  List _startContent = [];
  var _outLineTiles = [];
  int _counter = 0;
  int _selectedIndex;
  bool _isSelected;

  dynamic clearHilights(int currentIndex) {
    setState(() {
      _selectedIndex = currentIndex;
    });
  }

  updatedTiles() {
    if (_selectedIndex == null) {
      _selectedIndex = 0;
    }

    _currentContent = widget.currentContent;
    _name = widget.name;
    _startContent = widget.startContent;
    _counter = 0;
    _outLineTiles = [];
    _startContent.forEach((element) {
      _isSelected = _selectedIndex == _counter ? true : false;
      _outLineTiles.add(OutlineTile(
        key: Key("myKey$_counter"),
        outlineName: element[0],
        myContent: element[1],
        myIndex: _counter,
        onTileSelected: clearHilights,
        isSelected: _isSelected,
      ));
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    updatedTiles();
    return Scaffold(
        body: Center(
      child: Column(children: [
        SizedBox(height: 100),
        Text("Outline", style: new TextStyle(fontSize: 15)),
        Container(
          height: 215,
          width: 300,
          decoration: BoxDecoration(
            border: Border.all(
              color: Colors.lightGreenAccent,
              width: 2,
            ),
            borderRadius: BorderRadius.circular(2),
          ),
          child: ListView.builder(
            itemCount: _startContent.length,
            itemBuilder: (context, index) {
              return _outLineTiles[index];
            },
          ),
        ),
      ]),
    ));
  }
}

and finally is outline_tile.dart

import 'package:flutter/material.dart';

class OutlineTile extends StatefulWidget {
  final Key key;
  final String outlineName;
  final Icon myIcon;
  final String myContent;
  final int myIndex;
  final Function(int) onTileSelected;
  final bool isSelected;

  OutlineTile(
      {this.key,
      this.outlineName,
      this.myIcon,
      this.myContent,
      this.myIndex,
      this.onTileSelected,
      this.isSelected});

  @override
  _OutlineTileState createState() => _OutlineTileState();
}

class _OutlineTileState extends State<OutlineTile> {
  Color color;
  // Key _myKey;
  bool _isSelected;

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

    _isSelected = widget.isSelected;

    if (_isSelected == true) {
      color = Colors.lightBlueAccent;
    } else {
      color = Colors.transparent;
    }
  }

  void deselect() {
    setState(() {
      _isSelected = widget.isSelected;

      if (_isSelected == true) {
        color = Colors.lightBlueAccent;
      } else {
        color = Colors.transparent;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 4.0),
      child: Row(
        children: [
          Card(
            elevation: 10,
            margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
            child: SizedBox(
              width: 180,
              child: Container(
                color: color,
                child: ListTile(
                  title: Text(widget.outlineName),
                  onTap: () {
                    if (widget.outlineName == "Heading") {
                      Text("Called Heading");
                    } else if (widget.outlineName == "Paragraph") {
                      Text("Called Paragraph");
                    } else if (widget.outlineName == "Signature") {
                      Text("Called Signature");
                    } else {
                      Text("Called Image");
                    }
                    var _myIndex = widget.myIndex;

                    widget.onTileSelected(_myIndex);
                    deselect();
                  },
                ),
              ),
            ),
          ),
          SizedBox(
            height: 60,
            child: Column(
              children: [
                SizedBox(
                  height: 20,
                  child: IconButton(
                      iconSize: 30,
                      icon: Icon(Icons.arrow_drop_up),
                      onPressed: () {
                        print("Move Up");
                      }),
                ),
                SizedBox(height: 5),
                SizedBox(
                  height: 20,
                  child: IconButton(
                      iconSize: 30,
                      icon: Icon(Icons.arrow_drop_down),
                      onPressed: () {
                        print("Move Down");
                      }),
                ),
              ],
            ),
          ),
          SizedBox(
            height: 60,
            child: Column(
              children: [
                SizedBox(
                  height: 20,
                  child: IconButton(
                      iconSize: 20,
                      icon: Icon(Icons.add_box),
                      onPressed: () {
                        print("Add another");
                      }),
                ),
                SizedBox(
                  height: 10,
                ),
                SizedBox(
                  height: 20,
                  child: IconButton(
                      iconSize: 20,
                      icon: Icon(Icons.delete),
                      onPressed: () {
                        print("Delete");
                      }),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Thanks again

like image 299
Stephen S Avatar asked Oct 30 '20 14:10

Stephen S


1 Answers

Instead of manually deselecting tiles, just keep track of which tile is currently selected.

I've made a simple example for you. When we click a tile, we just set the selected index to the index we clicked, and each tile looks at that to see if its the currently selected tile.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(body: Home()),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int selectedIndex;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('Item: $index'),
          tileColor: selectedIndex == index ? Colors.blue : null,
          onTap: () {
            setState(() {
              selectedIndex = index;
            });
          },
        );
      },
    );
  }
}
like image 81
phimath Avatar answered Sep 21 '22 00:09

phimath