caduandrade / multi_split_view

Provides horizontal or vertical multiple split view for Flutter.
https://caduandrade.github.io/multi_split_view/
MIT License
128 stars 24 forks source link

MultiSplitViewController throwing exception when being built or modified to have no areas. #73

Closed cliftonlabrum closed 1 week ago

cliftonlabrum commented 1 week ago

I am setting up a MultiSplitView in a StatelessWidget like this:

MultiSplitView splitView = MultiSplitView(
  controller: UI.to.splitViewController,
  initialAreas: [
    Area(builder: (context, area) => Draft.blue()),
    Area(builder: (context, area) => Draft.yellow()),
    Area(builder: (context, area) => Draft.green())
  ],
);

MultiSplitViewTheme splitViewTheme = MultiSplitViewTheme(
  data: MultiSplitViewThemeData(dividerThickness: 17),
  child: splitView,
);

And then over in a watch_it controller, I initialize my Area widgets like this:

class UI with ChangeNotifier {
  static UI get to => di<UI>();

  setup(){
    splitViewController.areas = [
      Area(builder: (context, area) => Draft.blue()),
      Area(builder: (context, area) => Draft.yellow()),
      Area(builder: (context, area) => Draft.green())
    ];
  }
}

This results in an exception when my app launches:

The following StateError was thrown building LayoutBuilder:
Bad state: No element

The relevant error-causing widget was:
    MultiSplitView MultiSplitView:file:///Users/.../desktop.dart:12:32

When the exception was thrown, this was the stack:
#0    List.last (dart:core-patch/growable_array.dart:348:5)
growable_array.dart:348
#1    LayoutConstraints.adjustAreas (package:multi_split_view/src/internal/layout_constraints.dart:129:44)
layout_constraints.dart:129
#2    _MultiSplitViewState.build.<anonymous closure> (package:multi_split_view/src/multi_split_view.dart:193:28)
multi_split_view.dart:193
#3    _LayoutBuilderElement._layout.layoutCallback (package:flutter/src/widgets/layout_builder.dart:139:77)
layout_builder.dart:139
...

The desktop.dart:12 reference just points to my initializer:

MultiSplitView splitView = MultiSplitView( ...

After I move past the exception, the split view renders correctly and works, but I'm trying to avoid having this error. Can I not initialize it via a controller like this? Any idea what I'm doing wrong?

caduandrade commented 1 week ago

Hi @cliftonlabrum!

Either you configure the areas via controller or via initialAreas (which will generate an internal controller). But you can't use both at the same time, okay?

cliftonlabrum commented 1 week ago

Thanks for your quick reply! 😊 I wondered about that and tried it. If I delete initialAreas so I have this:

MultiSplitView splitView = MultiSplitView(
  controller: UI.to.splitViewController,
);

...I still get the same exception. πŸ˜…

If I do the opposite and only use initialAreas, it works without crashing. But I need a controller since I dynamically set my Area flex values depending on which page of my app is shown.

cliftonlabrum commented 1 week ago

It used to work for me in version 2.4.0. I had it in a StatelessWidget and controlled the state via a ChangeNotifier. It seems the internal and external state may be in conflict. πŸ€”

cliftonlabrum commented 1 week ago

Actually, the line of code it's blaming for the issue is slightly different if I use only a controller:

The relevant error-causing widget was:
    LayoutBuilder LayoutBuilder:
file:///.../.pub-cache/hosted/pub.dev/multi_split_view-3.1.0/lib/src/multi_split_view.dart:175:12
caduandrade commented 1 week ago

I believe there is some state problem with this watch_it package.

I made this code using the simple static package.

import 'package:flutter/material.dart';
import 'package:multi_split_view/multi_split_view.dart';
import 'package:watch_it/watch_it.dart';

void main() {
  // UI.to.setup();
  UI.to2.setup();
  runApp(const MultiSplitViewExampleApp());
}

class UI with ChangeNotifier {
  // static UI get to => di<UI>();

  static UI to2 = UI();

  final MultiSplitViewController controller = MultiSplitViewController();

  setup() {
    controller.areas = [
      Area(builder: (context, area) => Draft.blue()),
      Area(builder: (context, area) => Draft.yellow()),
      Area(builder: (context, area) => Draft.green())
    ];
  }
}

class MultiSplitViewExampleApp extends StatelessWidget {
  const MultiSplitViewExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MultiSplitViewExample(),
    );
  }
}

class MultiSplitViewExample extends StatefulWidget {
  const MultiSplitViewExample({Key? key}) : super(key: key);

  @override
  MultiSplitViewExampleState createState() => MultiSplitViewExampleState();
}

class MultiSplitViewExampleState extends State<MultiSplitViewExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Multi Split View Example')),
        body: MultiSplitViewTheme(
            data: MultiSplitViewThemeData(
                dividerPainter: DividerPainters.grooved2()),
            child: MultiSplitView(controller: UI.to2.controller)));
  }
}

Using the watch_it package, I get the error:

Rejecting promise with error: Bad state: GetIt: Object/factory with type UI is not registered inside GetIt. 
(Did you accidentally do GetIt sl=GetIt.instance(); instead of GetIt sl=GetIt.instance;
Did you forget to register it?)
cliftonlabrum commented 1 week ago

Try registering it first like this:

void main() {
  di.registerSingleton<UI>(UI());
  UI.to2.setup();
  runApp(const MultiSplitViewExampleApp());
}
caduandrade commented 1 week ago

:thinking: Now it's working here... I'm using multi_split_view 3.2.0

import 'package:flutter/material.dart';
import 'package:multi_split_view/multi_split_view.dart';
import 'package:watch_it/watch_it.dart';

void main() {
  di.registerSingleton<UI>(UI());
  UI.to.setup();
  //UI.to2.setup();
  runApp(const MultiSplitViewExampleApp());
}

class UI with ChangeNotifier {
  static UI get to => di<UI>();

  //static UI to2 = UI();

  final MultiSplitViewController controller = MultiSplitViewController();

  setup() {
    controller.areas = [
      Area(builder: (context, area) => Draft.blue()),
      Area(builder: (context, area) => Draft.yellow()),
      Area(builder: (context, area) => Draft.green())
    ];
  }
}

class MultiSplitViewExampleApp extends StatelessWidget {
  const MultiSplitViewExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MultiSplitViewExample(),
    );
  }
}

class MultiSplitViewExample extends StatefulWidget {
  const MultiSplitViewExample({Key? key}) : super(key: key);

  @override
  MultiSplitViewExampleState createState() => MultiSplitViewExampleState();
}

class MultiSplitViewExampleState extends State<MultiSplitViewExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Multi Split View Example')),
        body: MultiSplitViewTheme(
            data: MultiSplitViewThemeData(
                dividerPainter: DividerPainters.grooved2()),
            child: MultiSplitView(controller: UI.to.controller)));
  }
}

This code doesn't work for you?

flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[βœ“] Flutter (Channel stable, 3.19.6, on Ubuntu 22.04.4 LTS 6.5.0-35-generic, locale en_US.UTF-8)
[βœ“] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
[βœ“] Chrome - develop for the web
[βœ“] Linux toolchain - develop for Linux desktop
[βœ“] Android Studio (version 2023.2)
[βœ“] IntelliJ IDEA Community Edition (version 2023.3)
[βœ“] IntelliJ IDEA Community Edition (version 2022.3)
[βœ“] VS Code (version 1.73.1)
[βœ“] Connected device (2 available)
[βœ“] Network resources

β€’ No issues found!
cliftonlabrum commented 1 week ago

Your sample works for me. If I reduce my app to a really simple sample, it still crashes.

It's crashing on this line:

Area area = controllerHelper.areas.last;

...because areas is temporarily empty so the last causes an array crash.

This fixes it for me:

if (controllerHelper.areas.isNotEmpty) {
  Area area = controllerHelper.areas.last;
}

The only thing I can think of is that it's some kind of race condition where the areas aren't ready yet.

caduandrade commented 1 week ago

Apparently the method is just not prepared for a list of empty areas. I added the button to remove all areas and an error occurred. It's the same right?

import 'package:flutter/material.dart';
import 'package:multi_split_view/multi_split_view.dart';
import 'package:watch_it/watch_it.dart';

void main() {
  di.registerSingleton<UI>(UI());
  UI.to.setup();
  //UI.to2.setup();
  runApp(const MultiSplitViewExampleApp());
}

class UI with ChangeNotifier {
  static UI get to => di<UI>();

  //static UI to2 = UI();

  final MultiSplitViewController controller = MultiSplitViewController();

  setup() {
    controller.areas = [
      Area(builder: (context, area) => Draft.blue()),
      Area(builder: (context, area) => Draft.yellow()),
      Area(builder: (context, area) => Draft.green())
    ];
  }
}

class MultiSplitViewExampleApp extends StatelessWidget {
  const MultiSplitViewExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MultiSplitViewExample(),
    );
  }
}

class MultiSplitViewExample extends StatefulWidget {
  const MultiSplitViewExample({Key? key}) : super(key: key);

  @override
  MultiSplitViewExampleState createState() => MultiSplitViewExampleState();
}

class MultiSplitViewExampleState extends State<MultiSplitViewExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Multi Split View Example'), actions: [
          ElevatedButton(onPressed: _clear, child: const Text('Clear'))
        ]),
        body: MultiSplitViewTheme(
            data: MultiSplitViewThemeData(
                dividerPainter: DividerPainters.grooved2()),
            child: MultiSplitView(controller: UI.to.controller)));
  }

  void _clear() {
    UI.to.controller.areas = [];
  }
}

Error

======== Exception caught by widgets library =======================================================
The following StateError was thrown building LayoutBuilder:
Bad state: No element

The relevant error-causing widget was: 
  LayoutBuilder LayoutBuilder:file:///cadu/multi_split_view/lib/src/multi_split_view.dart:175:12
When the exception was thrown, this was the stack: 
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 297:3       throw_
dart-sdk/lib/_internal/js_dev_runtime/private/js_array.dart 356:5                 last]
packages/multi_split_view/src/internal/layout_constraints.dart 130:37             adjustAreas

I'll fix it.

caduandrade commented 1 week ago

Version 3.2.1 released.

Tks @cliftonlabrum!

cliftonlabrum commented 1 week ago

Awesome, thank you!