I’m learning flutter and trying to implement a screen with a fixed component and the rest scrollable. The SingleChildScrollView class looks like what I want but I cannot get it to work. This is a personal learning project where I’m trying to develop a inventory management system using barcodes with the package mobile_scanner. I’m testing on my android phone and the below example has duplicates fields to exaggerate the problem – I want the camera scanner box/preview to be fixed at the top of the screen and all the other components (should they be in a form?) to be scrollable. For example, receiving inventory screen may have multiple barcodes to know exactly where an item is stored (barcode on item, barcode on bin item is in, barcode on shelf, barcode on cabinet). Removing inventory screen may just be the item barcode. My code always scrolls the entire screen regardless of the widgets I use
This may need to be another question but I’ll mention it here. A better implementation would be to use barcode reading as a new widget/screen but I do not understand the state management enough yet. I’m using package auto_route I’ll try with this cookbook example. I imagine my fixed & scrolling problem will happen here too..
main.dart
import 'package:flutter/material.dart';
import 'package:example/example_screen.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ExampleScreen(),
),
),
);
}
}
example_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:auto_route/auto_route.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
// @RoutePage()
class ExampleScreen extends StatefulWidget {
ExampleScreen();
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
autoStart: false,
torchEnabled: false,
useNewCameraSelector: true,
);
final String tag = 'EXAMPLE';
Barcode? _barcode;
int activeBarcodeNum = 0;
StreamSubscription<Object?>? _subscription;
String? tmpDisplay;
final textController1 = TextEditingController();
final textController2 = TextEditingController();
void _handleBarcode(BarcodeCapture barcodes) {
Barcode? tmpCode = barcodes.barcodes.firstOrNull;
final rawVal = tmpCode?.rawValue.toString();
// TODO: do some logic
// if (rawVal!.contains('TYPE')) {
// check current values
setState(() {
_barcode = tmpCode;
controller.stop();
activeBarcodeNum = 0;
});
// }
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(_handleBarcode);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(_handleBarcode);
// auto start controller on app resume
// unawaited(controller.start());
case AppLifecycleState.inactive:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Example screen"),
backgroundColor: Colors.deepOrange[400],
),
body: SingleChildScrollView(
child: IntrinsicHeight(
child: Column(
children: [
// scanner box
SizedBox(
height: 500,
child: MobileScanner(
controller: controller,
),
),
Expanded(
child: Column(
children: [
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Padding(
//padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: TextField(
controller: textController1,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
], // Only numbers can be entered
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Quantity',
hintText: 'Quantity'
),
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: TextField(
controller: textController2,
maxLines: 5,
decoration: InputDecoration(
hintText: "Enter notes here",
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.black,
width: 2,
),
borderRadius: BorderRadius.circular(15),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.red,
width: 2,
),
borderRadius: BorderRadius.circular(10),
),
),
),
),
Container(
height: 70,
width: 250,
decoration: BoxDecoration(
color: Colors.green, borderRadius: BorderRadius.circular(20)),
child: TextButton(
onPressed: () {
final quantStr = textController1.text;
if (quantStr.isNotEmpty) {
// TODO: do stuff with the values
}
// back to home screen
// AutoRouter.of(context).popAndPush(HomeRoute());
},
child: const Text(
'SUBMIT',
style: TextStyle(color: Colors.white, fontSize: 25),
),
)
)
],
),
),
],
),
),
),
);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
}
}
2
Answers
If you want the first widget to be fixed/pinned on the screen, you may simply just extract the SingleChildScrollView to one widget lower in the tree.
Currently, you have a
SingleChildScrollView
as the root widget:which makes the enitre screen scrollable.
Instead, move the
SingleChildScrollView
from the root widget down to the nextColumn
:Here’s a complete runnable example:
put SingleChildScrollView to the Column which you need to be scrollable. here you are applying it to every children in the body of scaffold.