I am testing a simple scenario: Having many post categories, where each category has its own posts.
I want to be able to drag&drop and reorder the categories, but also the posts within a specific category.
I want to show fully extended all the lists inside a single scrollbar.
Yes it looks like I need one reorderablelistview for all the categories so I can reorder them, and for every category another reorderablelistview for ordering the posts within that category.
PS: Moving posts to another category by dragging would be awesome too
I will provide below some code that I am trying without success:
import 'dart:math';
import 'package:flutter/material.dart';
class TmpScreen extends StatefulWidget {
const TmpScreen({super.key});
@override
State<TmpScreen> createState() => _TmpScreenState();
}
class _TmpScreenState extends State<TmpScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Text('Posts'),
],
)),
body: SingleChildScrollView(
child: Container(
height: MediaQuery.of(context).size.height,
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text('All posts'),
const SizedBox(
height: 36,
),
Expanded(
child: ReorderableListView(
onReorder: (oldIndex, newIndex) {
print(
'Reordering post categories $oldIndex -> $newIndex');
},
children: List.generate(postsByCategory.length, (index) {
final postByCategory = postsByCategory[index];
return Column(
key: ValueKey(postByCategory.category),
children: [
Text(postByCategory.category),
Expanded(
child: ReorderableListView(
onReorder: (oldIndex, newIndex) {
print(
'Reordering posts inside category ${postByCategory.category}, $oldIndex -> $newIndex');
},
children: List.generate(
postByCategory.posts.length, (index) {
final post = postByCategory.posts[index];
return Text(
key: ValueKey(post.name),
'Post ${post.name}');
})))
],
);
})),
)
],
)),
),
);
}
}
final postsByCategory =
List.generate(10, (index) => generateRandomPostsByCategory(10));
class PostsByCategory {
final String category;
final List<Post> posts;
PostsByCategory({required this.category, required this.posts});
}
class Post {
final String name;
Post(this.name);
}
String generateRandomString(int length) {
const String chars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
Random random = Random();
return String.fromCharCodes(
Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
),
);
}
Post generateRandomPost() {
String randomPostName = generateRandomString(10);
return Post(randomPostName);
}
PostsByCategory generateRandomPostsByCategory(int numPosts) {
String randomCategory =
generateRandomString(8); // Generate a random category name
List<Post> randomPosts = List.generate(numPosts, (_) => generateRandomPost());
return PostsByCategory(category: randomCategory, posts: randomPosts);
}
The code above does not render at all.
I can fix it to render but it just doesn’t work, the lists don’t show all the items and I don’t know how to support my use case.
Any Flutter expert can provide useful insights?
When nesting scrollable widgets like ReorderableListView
, especially with the same scroll direction, you often encounter layout constraint issues. This happens because Flutter cannot infer the correct height/width of the nested lists, leading to rendering problems.
To resolve this, you need to manually set the height of each category’s ReorderableListView
. The height should account for both the label height of the category and the total height of the posts within that category.
Also, it’s a good idea to disable scrolling for the posts within each category by setting physics
to NeverScrollableScrollPhysics
.
Here’s a simplified and corrected version of your code:
import 'package:flutter/material.dart';
void main() => runApp(const MainApp());
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('All Posts'),
),
body: const PostsView(),
),
);
}
}
class PostsView extends StatefulWidget {
const PostsView({super.key});
@override
State<PostsView> createState() => _PostsViewState();
}
class _PostsViewState extends State<PostsView> {
final _categories = [
PostsByCategory(
category: 'Category 1',
posts: [Post('Post 1'), Post('Post 2'), Post('Post 3')],
),
PostsByCategory(
category: 'Category 2',
posts: [Post('Post 4'), Post('Post 5'), Post('Post 6')],
),
PostsByCategory(
category: 'Category 3',
posts: [Post('Post 7'), Post('Post 8'), Post('Post 9')],
),
];
@override
Widget build(BuildContext context) {
const postHeight = 56.0; // Set a fixed height for each post
const labelHeight = 48.0; // Set a fixed height for each category label
return ReorderableListView.builder(
primary: true,
padding: const EdgeInsets.all(16),
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex -= 1;
final category = _categories.removeAt(oldIndex);
_categories.insert(newIndex, category);
},
itemCount: _categories.length,
itemBuilder: (_, index) {
final category = _categories[index];
return SizedBox(
key: ValueKey(category.category),
height: (category.posts.length * postHeight) + labelHeight, // Dynamically calculate height based on the number of posts
child: Column(
children: [
SizedBox(
height: labelHeight, // Fixed height for label
child: Center(
child: Text(category.category),
),
),
Expanded(
child: ReorderableListView.builder(
physics: const NeverScrollableScrollPhysics(), // Disable scrolling for the nested list
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex -= 1;
final post = category.posts.removeAt(oldIndex);
category.posts.insert(newIndex, post);
},
itemCount: category.posts.length,
itemBuilder: (_, index) {
final post = category.posts[index];
return SizedBox(
key: ValueKey(post.name),
height: postHeight, // Fixed height for each post
child: Center(
child: Text(post.name),
),
);
},
),
),
],
),
);
},
);
}
}
class PostsByCategory {
final String category;
final List<Post> posts;
PostsByCategory({required this.category, required this.posts});
}
class Post {
final String name;
Post(this.name);
}