Skip to content

Instantly share code, notes, and snippets.

@rubywai
Created June 9, 2025 02:47
Show Gist options
  • Save rubywai/7433634256c89e09efeecc5752f9f4b6 to your computer and use it in GitHub Desktop.
Save rubywai/7433634256c89e09efeecc5752f9f4b6 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CustomScrollView with Dual ListView',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CustomScrollViewWithDualList(),
debugShowCheckedModeBanner: false,
);
}
}
class CustomScrollViewWithDualList extends StatefulWidget {
@override
_CustomScrollViewWithDualListState createState() => _CustomScrollViewWithDualListState();
}
class _CustomScrollViewWithDualListState extends State<CustomScrollViewWithDualList> {
late ScrollController _leftController;
late ScrollController _rightController;
bool _isLeftScrolling = false;
bool _isRightScrolling = false;
// Sample data for both lists
final List<String> leftData = List.generate(50, (index) => 'Left Item $index');
final List<String> rightData = List.generate(50, (index) => 'Right Item $index');
@override
void initState() {
super.initState();
_leftController = ScrollController();
_rightController = ScrollController();
// Add listeners to sync scrolling
_leftController.addListener(_syncLeftToRight);
_rightController.addListener(_syncRightToLeft);
}
void _syncLeftToRight() {
if (!_isRightScrolling && _leftController.hasClients && _rightController.hasClients) {
_isLeftScrolling = true;
_rightController.jumpTo(_leftController.offset);
_isLeftScrolling = false;
}
}
void _syncRightToLeft() {
if (!_isLeftScrolling && _rightController.hasClients && _leftController.hasClients) {
_isRightScrolling = true;
_leftController.jumpTo(_rightController.offset);
_isRightScrolling = false;
}
}
@override
void dispose() {
_leftController.dispose();
_rightController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CustomScrollView with Dual ListView'),
elevation: 2,
),
body: CustomScrollView(
slivers: [
// Header section (optional)
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(16.0),
color: Colors.grey[100],
child: Text(
'Synchronized Dual ListView',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
textAlign: TextAlign.center,
),
),
),
// Main content: Row with two synchronized ListViews
SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).size.height * 0.6, // 60% of screen height
padding: EdgeInsets.all(8.0),
child: Row(
children: [
// Left ListView
Expanded(
child: Container(
margin: EdgeInsets.only(right: 4.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue[300]!),
borderRadius: BorderRadius.circular(8.0),
color: Colors.blue[50],
),
child: Column(
children: [
// Left ListView Header
Container(
width: double.infinity,
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.blue[200],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(7.0),
topRight: Radius.circular(7.0),
),
),
child: Text(
'Left Column',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
textAlign: TextAlign.center,
),
),
// Left ListView Content
Expanded(
child: ListView.builder(
controller: _leftController,
padding: EdgeInsets.all(8.0),
itemCount: leftData.length,
itemBuilder: (context, index) {
return Container(
height: 60,
margin: EdgeInsets.only(bottom: 8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.0),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 2,
offset: Offset(0, 1),
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
leftData[index],
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
Text(
'Data: ${index + 1}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
);
},
),
),
],
),
),
),
// Right ListView
Expanded(
child: Container(
margin: EdgeInsets.only(left: 4.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.green[300]!),
borderRadius: BorderRadius.circular(8.0),
color: Colors.green[50],
),
child: Column(
children: [
// Right ListView Header
Container(
width: double.infinity,
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.green[200],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(7.0),
topRight: Radius.circular(7.0),
),
),
child: Text(
'Right Column',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
textAlign: TextAlign.center,
),
),
// Right ListView Content
Expanded(
child: ListView.builder(
controller: _rightController,
padding: EdgeInsets.all(8.0),
itemCount: rightData.length,
itemBuilder: (context, index) {
return Container(
height: 60,
margin: EdgeInsets.only(bottom: 8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.0),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 2,
offset: Offset(0, 1),
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
rightData[index],
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
Text(
'Value: ${(index + 1) * 2}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
);
},
),
),
],
),
),
),
],
),
),
),
// Footer - appears after the Row content ends
SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.all(16.0),
padding: EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: Colors.grey[300]!),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
children: [
Text(
'Footer Section',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFooterButton(
'Scroll to Top',
Icons.keyboard_arrow_up,
Colors.blue,
() {
_leftController.animateTo(
0,
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
},
),
_buildFooterButton(
'Scroll to Middle',
Icons.remove,
Colors.orange,
() {
if (_leftController.hasClients) {
double middle = _leftController.position.maxScrollExtent / 2;
_leftController.animateTo(
middle,
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
},
),
_buildFooterButton(
'Scroll to Bottom',
Icons.keyboard_arrow_down,
Colors.green,
() {
if (_leftController.hasClients) {
_leftController.animateTo(
_leftController.position.maxScrollExtent,
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
},
),
],
),
SizedBox(height: 12),
Text(
'Both lists scroll together synchronously',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
],
),
),
),
// Additional content can be added here
SliverToBoxAdapter(
child: Container(
height: 100,
margin: EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.purple[50],
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.purple[200]!),
),
child: Center(
child: Text(
'Additional Content Section\n(This shows the CustomScrollView continues)',
style: TextStyle(
fontSize: 16,
color: Colors.purple[700],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
),
),
],
),
);
}
Widget _buildFooterButton(String text, IconData icon, Color color, VoidCallback onPressed) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 18),
label: Text(
text,
style: TextStyle(fontSize: 12),
),
style: ElevatedButton.styleFrom(
backgroundColor: color.withOpacity(0.1),
foregroundColor: color,
padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
side: BorderSide(color: color),
),
elevation: 2,
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment