It is a bug of a ListView
inside another ListView
.
A simple Scaffold
with the body showing a ListView
(it doesn’t matter if it is a ListView
or a ListView.builder
, both will have the same behavior, therefore, the same bug.) with a Column
widget that has two widgets inside: a StatefulWidget
and another ListView.builder
widget, and this ListView.builder
widget inside the Column
widget has a ListTile
widget that only displays information: text with the index and a delete icon. While the StatefulWidget
contains another ListView.builder
.
The StatefulWidget
gets "STUCK" in a specific position. Not a position in the list that controls the ListView
widget, but in the layout itself, which is extremely bizarre. Even if you delete the Column
widget that would have the StatefulWidget
, it simply doesn’t disappear, it remains, regardless of whether you use const
or not when calling the StatefulWidget
.
I created a simple DartPad with only 150 lines of code, with the code that I will share with you below so that everyone can test it.
Change the Flutter version in DartPad (Main, Stable, Beta) and always the same behavior. I will not under any circumstances change the Flutter installed on my computer, as I have edits to the files that I need to keep for very specific projects of my clients.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ListViewWithDelete(),
);
}
}
class ListViewWithDelete extends StatefulWidget {
@override
_ListViewWithDeleteState createState() => _ListViewWithDeleteState();
}
class _ListViewWithDeleteState extends State<ListViewWithDelete> {
List<String> items = List<String>.generate(10, (index) => 'Item ${index + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView com Botão de Excluir'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Column(
children: [
AulaWidget(),
ListTile(
leading: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
items.removeAt(index);
});
},
),
title: Text(items[index]),
),
],
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
setState(() {
items.add('Item ${items.length + 1}');
});
},
child: Text('Adicionar Item'),
),
),
],
),
);
}
}
class AulaWidget extends StatefulWidget {
@override
_AulaWidgetState createState() => _AulaWidgetState();
}
class _AulaWidgetState extends State<AulaWidget> {
List<String> listaDeAulas = List<String>.generate(5, (index) => 'Aula $index');
@override
Widget build(BuildContext context) {
return Column(
children: [
ListView.builder(
itemCount: listaDeAulas.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
listaDeAulas.removeAt(index);
});
},
),
title: Text("Sinais de Trânsito ${listaDeAulas[index]}"),
);
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
listaDeAulas.add('Aula ${listaDeAulas.length}');
});
},
child: const Text('Adicionar Aula'),
),
),
],
),
],
);
}
void _showConfirmationDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Adicionar Nova Aula"),
content: const Text("Deseja adicionar uma nova aula?"),
actions: <Widget>[
TextButton(
child: const Text("Cancelar"),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text("Confirmar"),
onPressed: () {
setState(() {
listaDeAulas.add('Aula ${listaDeAulas.length}');
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
I realized that I was unable to correctly express the behavior of the code, since the responses I have received so far have only been about the list called listaDeAulas with some solutions that were not tested before being presented here in my post.
So I will kindly ask you in a very earnest manner to copy the code I provided and put it in DartPad. Do not make any changes to the code and do not worry about the Flutter versions. And then click on the button to add classes in a single item. Then try to delete the item that you just added classes to and notice that the classes that are part of, that are linked to, that specific item you wanted simply do not disappear, they remain fixed, including in the same position in the list.
I have already implemented the solutions provided so far: 09/20/2024 and none of them worked, because I believe I had not expressed myself better so that you would understand the importance of having observed the behavior of the code in DartPad, I hope I have been clear enough now.
Today 22.09.2024 I was able to find a solution.
Thanks to @pskink tireless efforts, I was finally able to resolve the issue. I’ll put the complete code here with the final solution so you can reproduce it via DartPad.
And a constructive criticism to those who downvoted my post: if you thought my post was not positive for the community, you are wrong, because what I proved in DartPad is in fact a bug and I will report it to the Flutter team to improve Flutter. When the solution is implemented by the Flutter team I will make sure to come back to this post to reference it, just to prove once and for all that you who downvoted my post are wrong.
The fullcode (still in development):
// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
),
home: const CriarCursoProfessorModulo(),
);
}
}
class CriarCursoProfessorModulo extends StatefulWidget {
const CriarCursoProfessorModulo({super.key});
@override
State<CriarCursoProfessorModulo> createState() =>
_CriarCursoProfessorModuloState();
}
class _CriarCursoProfessorModuloState extends State<CriarCursoProfessorModulo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isExpanded = true;
Map<ModuloModel, List<AulaModel>> listaDeModulosAulas = {};
void _toggleExpansion() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.reverse();
} else {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animation = Tween<double>(begin: 1, end: 0).animate(_controller);
// Exemplo de módulo inicial
ModuloModel moduloModel = ModuloModel(
idModulo: "1",
titulo: "Módulo 1",
aulas: [],
materialDeApoio: [],
criadoem: DateTime.now(),
atualizadoem: DateTime.now(),
);
listaDeModulosAulas.addAll({
moduloModel: [],
});
}
@override
Widget build(BuildContext context) {
bool lightMode =
MediaQuery.of(context).platformBrightness == Brightness.light;
return Scaffold(
backgroundColor: lightMode ? Colors.white : Colors.white,
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
TextButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
const Color(0xff029846),
),
shape: WidgetStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
)),
padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.only(
top: 10,
bottom: 10,
left: 16,
right: 16,
),
),
),
onPressed: () {
ModuloModel moduloModel = ModuloModel(
idModulo: "",
titulo: "",
aulas: [],
materialDeApoio: [],
criadoem: DateTime.now(),
atualizadoem: DateTime.now(),
);
setState(() {
listaDeModulosAulas.addAll({
moduloModel: [],
});
});
},
icon: const Icon(
Icons.add,
color: Colors.white,
),
label: const SizedBox(
child: Text(
"Novo módulo",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
color: Color(0xffFFFFFF),
),
),
),
),
ListView.builder(
shrinkWrap: true,
itemCount: listaDeModulosAulas.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
ModuloModel modulo =
listaDeModulosAulas.keys.elementAt(index);
List<AulaModel> aulas =
listaDeModulosAulas.values.elementAt(index);
return Card(
elevation: 0.0,
color: Colors.white,
borderOnForeground: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
side: const BorderSide(
color: Color(0xffA9A9A9),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize
.min, // Permite que a Column encolha
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
modulo.titulo,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Color(0xff404040),
),
),
IconButton(
onPressed: _toggleExpansion,
icon: AnimatedSwitcher(
duration:
const Duration(milliseconds: 300),
transitionBuilder: (Widget child,
Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
},
child: _isExpanded
? const Icon(
Icons.keyboard_arrow_up_outlined)
: const Icon(Icons
.keyboard_arrow_down_outlined),
),
),
],
),
SizeTransition(
// Anima o tamanho do botão
sizeFactor: _animation,
child: Column(
children: [
AulaWidget(
aulas: aulas,
aoMudarAula: (novasAulas) {
setState(() {
listaDeModulosAulas[modulo] =
novasAulas;
});
},
),
TextButton(
style: ButtonStyle(
backgroundColor:
WidgetStateProperty.all<Color>(
const Color(0xffF44336)
.withOpacity(0.08),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12),
),
),
padding:
WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.only(
top: 13,
bottom: 13,
left: 28.5,
right: 28.5,
),
),
),
onPressed: () {
setState(() {
listaDeModulosAulas.remove(modulo);
});
},
child: const SizedBox(
width: double.infinity,
child: Text(
"Excluir módulo",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w900,
color: Color(0xffF44336),
),
),
),
),
const SizedBox(
height: 16,
),
],
),
),
],
),
),
);
},
),
],
),
),
],
),
),
);
}
}
class AulaWidget extends StatefulWidget {
final List<AulaModel>? aulas;
final ValueChanged<List<AulaModel>>? aoMudarAula;
const AulaWidget({
super.key,
this.aulas,
this.aoMudarAula,
});
@override
State<AulaWidget> createState() => _AulaWidgetState();
}
class _AulaWidgetState extends State<AulaWidget> {
final FocusScopeNode _focusScopeNode = FocusScopeNode();
@override
void dispose() {
_focusScopeNode.dispose();
super.dispose();
}
// Função para adicionar uma aula
void _adicionarAula() {
setState(() {
widget.aulas!.add(AulaModel(
idAula: "1",
titulo: "Nova Aula",
descricao: "",
criadoem: DateTime.now(),
atualizadoem: DateTime.now(),
));
widget.aoMudarAula!(widget.aulas!);
});
}
// Função para remover uma aula
void _removerAula(int index) {
setState(() {
widget.aulas!.removeAt(index);
widget.aoMudarAula!(widget.aulas!);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ListView.builder(
itemCount: widget.aulas!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
AulaModel aulaModel = widget.aulas!.elementAt(index);
return Container(
decoration: BoxDecoration(
color: const Color(0xffF6F6F6),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.only(
top: 17,
bottom: 17,
right: 16,
left: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
const Icon(
Icons.play_circle,
color: Color(0xff2196F3),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
aulaModel.titulo,
style: const TextStyle(
color: Color(0xff2196F3),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.edit,
color: Color(0xff2A2E45),
),
),
IconButton(
onPressed: () {
_removerAula(index);
},
icon: const Icon(
Icons.delete,
color: Color(0xffF44336),
),
),
],
),
],
),
);
},
),
const SizedBox(
height: 16,
),
Row(
children: [
Expanded(
child: TextButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
const Color(0xff029846).withOpacity(0.08),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.only(
top: 13,
bottom: 13,
left: 28.5,
right: 28.5,
),
),
),
onPressed: () {
_adicionarAula();
},
child: const SizedBox(
width: double.infinity,
child: Text(
"Nova aula",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
color: Color(0xff029846),
),
),
),
),
),
const SizedBox(
width: 16,
),
],
),
],
);
}
}
class AulaModel {
final String idAula;
final String titulo;
final String descricao;
final DateTime criadoem;
final DateTime atualizadoem;
// Construtor a partir de um Map
AulaModel({
required this.idAula,
required this.titulo,
required this.descricao,
required this.criadoem,
required this.atualizadoem,
});
AulaModel.fromMap(Map<String, dynamic> map)
: assert(map["idAula"] != null),
assert(map["titulo"] != null),
assert(map["descricao"] != null),
assert(map["criadoem"] != null),
assert(map["atualizadoem"] != null),
idAula = map["idAula"],
titulo = map["titulo"],
descricao = map["descricao"],
criadoem = map["criadoem"],
atualizadoem = map["atualizadoem"];
}
class ModuloModel {
final String idModulo;
final String titulo;
final List<String> aulas;
final List<String> materialDeApoio;
final DateTime criadoem;
final DateTime atualizadoem;
// Construtor a partir de um Map
ModuloModel({
required this.idModulo,
required this.titulo,
required this.aulas,
required this.materialDeApoio,
required this.criadoem,
required this.atualizadoem,
});
ModuloModel.fromMap(Map<String, dynamic> map)
: assert(map["idModulo"] != null),
assert(map["titulo"] != null),
assert(map["aulas"] != null),
assert(map["materialDeApoio"] != null),
assert(map["criadoem"] != null),
assert(map["atualizadoem"] != null),
idModulo = map["idModulo"],
titulo = map["titulo"],
aulas = map["aulas"],
materialDeApoio = map["materialDeApoio"],
criadoem = map["criadoem"],
atualizadoem = map["atualizadoem"];
}
2
Answers
I'm creating this comment to close the post here on StackOverflow.
The temporary solution is to work with a list of type Map that will control both lists, even if the list is passed to a child Widget.
The complete solution is:
In short, You don’t need to use nested ListView, for inner ListView replace with Column and render the children .