skip to Main Content

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


  1. 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:

    Scaffold(
      appBar: AppBar(...),
      body: SingleChildScrollView( //--> SingleChildScrollView as top level
        child: IntrinsicHeight(
          child: Column(
            children: [
              SizedBox(
                height: 500,
                child: MobileScanner(...),
              ),
              Expanded(
                child: Column(
                  children: [
                   _buildScanButton(),
                   _buildScanButton(),  
                   ...
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    )
    

    which makes the enitre screen scrollable.

    Instead, move the SingleChildScrollView from the root widget down to the next Column:

    Scaffold(
          appBar: AppBar(...),
          body: Column(
            children: [
              SizedBox( // --> No `SingleChildScrollView`, it's moved down the tree
                height: 300,
                child: MobileScanner(
                  controller: controller,
                ),
              ),
              Expanded(
                child: SingleChildScrollView( // --> add the `SingleChildScrollView` here.
                  child: Column(
                    children: [
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
              ),
            ],
          ),
    

    Here’s a complete runnable example:

    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:mobile_scanner/mobile_scanner.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(),
            ),
          ),
        );
      }
    }
    
    class ExampleScreen extends StatefulWidget {
      const ExampleScreen({super.key});
    
      @override
      State<ExampleScreen> createState() => _ExampleScreenState();
    }
    
    class _ExampleScreenState extends State<ExampleScreen>
        with WidgetsBindingObserver {
      final MobileScannerController controller = MobileScannerController(
        autoStart: false,
        torchEnabled: false,
        useNewCameraSelector: true,
      );
    
      Barcode? _barcode;
      StreamSubscription<Object?>? _subscription;
    
      final textController1 = TextEditingController();
      final textController2 = TextEditingController();
    
      void _handleBarcode(BarcodeCapture barcodes) {
        final Barcode? tmpCode = barcodes.barcodes.firstOrNull;
        setState(() {
          _barcode = tmpCode;
          controller.stop();
        });
      }
    
      @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.resumed:
            _subscription = controller.barcodes.listen(_handleBarcode);
            break;
          case AppLifecycleState.inactive:
          case AppLifecycleState.detached:
          case AppLifecycleState.hidden:
          case AppLifecycleState.paused:
            unawaited(_subscription?.cancel());
            _subscription = null;
            unawaited(controller.stop());
            break;
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("Example Screen"),
            backgroundColor: Colors.deepOrange[400],
          ),
          body: Column(
            children: [
              SizedBox(
                height: 300,
                child: MobileScanner(
                  controller: controller,
                ),
              ),
              Expanded(
                child: SingleChildScrollView(
                  child: Column(
                    children: [
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      _buildScanButton(),
                      Padding(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 15, vertical: 10),
                        child: TextField(
                          controller: textController1,
                          keyboardType: TextInputType.number,
                          inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                          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
                            }
                          },
                          child: const Text(
                            'SUBMIT',
                            style: TextStyle(color: Colors.white, fontSize: 25),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        );
      }
    
      Widget _buildScanButton() {
        return Row(
          children: [
            Expanded(
              flex: 3,
              child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    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()}',
                ),
              ),
            ),
          ],
        );
      }
    
      @override
      Future<void> dispose() async {
        WidgetsBinding.instance.removeObserver(this);
        unawaited(_subscription?.cancel());
        _subscription = null;
        super.dispose();
        await controller.dispose();
      }
    }
    
    
    Login or Signup to reply.
  2. put SingleChildScrollView to the Column which you need to be scrollable. here you are applying it to every children in the body of scaffold.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search