✏️

[Flutter]NestedScrollViewのbodyで抱えた問題

2022/01/21に公開

こちらの記事は
抱えた問題が完全に解消できたわけではなく、記事にすることによって似た実装をしてる方がいればコメントでご教授いただきたいという気持ちで書いています。

やりたいこと

TabBar/TabViewでタブを使った画面を作り、TabBarはスクロールしたら隠れるようにしたい。
→NestedScrollViewを使い、headerSliverBuilderでTabBarをSliverAppBarで囲い実装
bodyにTabViewを使う

また各TabViewでScrollControllerにaddListenerしてoffsetを監視したい

エラー

TabView全てでScrollControllerをaddListenerすると、前のページがdisposeされる前に次がビルドされ、複数ページにscrollControllerが使われたことになってエラーが起きる

例外が発生しました
_AssertionError ('package:flutter/src/widgets/scroll_controller.dart': Failed assertion: line 108 pos 12: '_positions.length == 1': ScrollController attached to multiple scroll views.)

とった処置

やりたいことが全て満たして実装ができなかったため、SliverAppの機能を使わない実装とした。

このように、SliverAppBarを囲ったTabBarにTabViewではaddListenerするみたいな実装をしてる人いるんだろうか?

実装の中身

実装の中身でこんな感じにできたらいいなと思っているというのを書きました
なので動かしたらエラーが起きます

まずはTabBar

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: DefaultTabController(
        length: 3,
        child: SafeArea(
          child: NestedScrollView(
            key: globalKey,
            floatHeaderSlivers: true,
            headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
              return [
                SliverOverlapAbsorber(
                  handle:
                      NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  sliver: SliverAppBar(
                    forceElevated: innerBoxIsScrolled,
                    title: const TabBar(
                      isScrollable: true,
                      indicator: BoxDecoration(),
                      tabs: [
                        Text('りんご'),
                        Text('みかん'),
                        Text('レモン'),
                      ],
                    ),
                  ),
                )
              ];
            },
            body: TabBarView(
              children: [
                _Apple(),
                _Orange(),
                _Lemon(),
              ],
            ),
          ),
        ),
      ),
    );
  }

TabView

class _Apple extends StatefulWidget {
 
 State<StatefulWidget> createState() => _AppleState();
}

class _AppleState extends State<_Apple> {
 ScrollController scrollController = globalKey.currentState!.innerController;
 double _offset = 0.0;

 void _listener() {
   setState(() {
     _offset = scrollController.offset;
   });
 }

 
 void initState() {
   super.initState();
   // こんな感じに監視をしたい
   // 一つの画面ならいいが複数で監視するとエラーになる
   scrollController.addListener(_listener);
 }

 
 void dispose() {
   super.dispose();
   scrollController.removeListener(_listener);
 }

 
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text('$_offset'),
     ),
     body: ListView(
       children: List.generate(
         10,
         (index) => Container(
           height: 100,
           alignment: Alignment.center,
           child: Text("$index"),
         ),
       ),
     ),
   );
 }
}

全体像はこんな感じ

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: DefaultTabController(
        length: 3,
        child: SafeArea(
          child: NestedScrollView(
            key: globalKey,
            floatHeaderSlivers: true,
            headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
              return [
                SliverOverlapAbsorber(
                  handle:
                      NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  sliver: SliverAppBar(
                    forceElevated: innerBoxIsScrolled,
                    title: const TabBar(
                      isScrollable: true,
                      indicator: BoxDecoration(),
                      tabs: [
                        Text('りんご'),
                        Text('みかん'),
                        Text('レモン'),
                      ],
                    ),
                  ),
                )
              ];
            },
            body: TabBarView(
              children: [
                _Apple(),
                _Orange(),
                _Lemon(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _Apple extends StatefulWidget {
  
  State<StatefulWidget> createState() => _AppleState();
}

class _AppleState extends State<_Apple> {
  ScrollController scrollController = globalKey.currentState!.innerController;
  double _offset = 0.0;

  void _listener() {
    setState(() {
      _offset = scrollController.offset;
    });
  }

  
  void initState() {
    super.initState();
    // こんな感じに監視をしたい
    scrollController.addListener(_listener);
  }

  
  void dispose() {
    super.dispose();
    scrollController.removeListener(_listener);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('$_offset'),
      ),
      body: ListView(
        children: List.generate(
          10,
          (index) => Container(
            height: 100,
            alignment: Alignment.center,
            child: Text("$index"),
          ),
        ),
      ),
    );
  }
}

class _Orange extends StatefulWidget {
  
  State<StatefulWidget> createState() => _OrangeState();
}

class _OrangeState extends State<_Orange> {
  ScrollController scrollController = globalKey.currentState!.innerController;
  double _offset = 0.0;

  void _listener() {
    setState(() {
      _offset = scrollController.offset;
    });
  }

  
  void initState() {
    super.initState();
    scrollController.addListener(_listener);
  }

  
  void dispose() {
    super.dispose();
    scrollController.removeListener(_listener);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('$_offset'),
      ),
      body: ListView(
        children: List.generate(
          10,
          (index) => Container(
            height: 100,
            alignment: Alignment.center,
            child: Text("$index"),
          ),
        ),
      ),
    );
  }
}

class _Lemon extends StatefulWidget {
  
  State<StatefulWidget> createState() => _LemonState();
}

class _LemonState extends State<_Lemon> {
  ScrollController scrollController = globalKey.currentState!.innerController;
  double _offset = 0.0;

  void _listener() {
    setState(() {
      _offset = scrollController.offset;
    });
  }

  
  void initState() {
    super.initState();
    scrollController.addListener(_listener);
  }

  
  void dispose() {
    super.dispose();
    scrollController.removeListener(_listener);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('$_offset'),
      ),
      body: ListView(
        children: List.generate(
          10,
          (index) => Container(
            height: 100,
            alignment: Alignment.center,
            child: Text("$index"),
          ),
        ),
      ),
    );
  }
}

これができなくて、SliverAppBarを消しました。
いい方法ないかな〜

Discussion