
【Flutter】スティッキーヘッダーとグループ化されたリスト(MaterialとCupertino両対応)
2021.12.21
スティッキーヘッダーのリストをFlutterで作る!
今回はiOSでよく見る、以下のようなスティッキーヘッダーのリストをFlutterで作っていきたいと思います。
FlutterではSliverPersistentHeaderの pinned を使うことで同じような機能が実装できますが、こちらでは全てのヘッダーがスタックされるようになってしまいます。
他の手段としてプラグインではありますが、flutter_sticky_headerを利用することでスティッキーヘッダーの実装はできます。
しかしヘッダー同士で接触した時の非表示処理がフェードアウトとなっているのと、Cupertino(iOS)デザインに対応していません。
https://pub.dev/packages/flutter_sticky_header
また実際にこのようなリストを利用する際は、APIから受け取ったModelクラスを使うことが多いと思うので、grouped_listプラグインのようにグループ化しやすいよう実装していきたいと思います。
https://pub.dev/packages/grouped_list
スティッキーヘッダーを実装する
早速実装に入っていきます!
pubspecは以下のようにして進めていきます。
1 2 3 4 5 6 7 8 9 | environment: sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter provider: '>=5.0.0-nullsafety.5 <6.0.0' flutter_platform_widgets: |
flutter_platform_widgetsは、端末ごとにMaterialとCupertinoのデザインを自動的に表示してくれるプラグインです。
https://pub.dev/packages/flutter_platform_widgets
今回実装するクラスの呼び出しイメージ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class _ListData { final int id; final String title; _ListData({required this.id, required this.title}); } final list = List.generate(100, (index) => _ListData(id: index % 10, title: 'Title ${index.toString()}')); final listWidget = StickyGroupList( elements: list, groupBy: (_ListData element) => element.id, groupHeaderTitleBuilder: (int groupByValue) => PlatformText(groupByValue.toString()), itemBuilder: (context, _ListData element) => Container( margin: EdgeInsets.all(4), padding: EdgeInsets.all(12), child: PlatformText(element.title, style: TextStyle(color: Colors.black, fontSize: 18)), ), onRefresh: () async { // 更新処理をここに書く }, groupHeaderBackgroundColor: Colors.blue, ); |
ほぼgrouped_listと同じ感覚で使えるようにしています。
スティッキーヘッダーとグループ化されたリストの実装
まずは変数部分とイニシャライザ部分を作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class StickyGroupList<T, E extends Object> extends StatelessWidget { // リスト化したいModelを入れる final List<T> elements; // グループ化するための値を返す final E Function(T element) groupBy; // グループごとのHeaderのWidgetの中身を設定できる final Widget Function(E value) groupHeaderTitleBuilder; // リスト部分のWidgetを設定できる final Widget Function(BuildContext context, T element) itemBuilder; // リストを下に引っ張った時の更新処理を設定できる final Future<void> Function()? onRefresh; // ヘッダーの背景色 final Color? groupHeaderBackgroundColor; // ヘッダーの高さ final double groupHeaderHeight; StickyGroupList({ Key? key, required this.elements, required this.groupBy, required this.groupHeaderTitleBuilder, required this.itemBuilder, this.onRefresh, this.groupHeaderBackgroundColor, this.groupHeaderHeight = 60, }) : super(key: key); |
任意のModelクラスをグループ化をするため、ジェネリクスを利用しています。
Tがグループ化したいModelクラス、EがModel内で一意な値になるIDの型(大抵はintかString)になります。
grouped_listとだいたい同じ感じのイニシャライザになりましたね。
次に build 周りを作成していきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | final Map<E, GlobalObjectKey> _headerGlobalKeys = {}; final _scrollController = ScrollController(); @override Widget build(BuildContext context) { var itemGroups = _itemGroups; itemGroups.keys .forEach((key) => _headerGlobalKeys[key] = GlobalObjectKey(key)); return ListenableProvider( create: (_) { final viewModel = StickyGroupListViewModel(headerDefaultHeight: groupHeaderHeight, keys: _headerGlobalKeys.values.toList()); _scrollController.addListener(() => _scrollListener(viewModel)); return viewModel; }, dispose: _dispose, child: _SliverGroupList( groupHeaderTitleBuilder: groupHeaderTitleBuilder, itemBuilder: itemBuilder, onRefresh: onRefresh, groupHeaderBackgroundColor: groupHeaderBackgroundColor, itemGroups: itemGroups, headerGlobalKeys: _headerGlobalKeys, scrollController: _scrollController, ), ); } Map<E, List<T>> get _itemGroups => elements.fold(Map<E, List<T>>(), (itemGroup, element) { final group = groupBy(element); var groupItem = itemGroup[group]; if (groupItem != null) { groupItem.add(element); itemGroup[group] = groupItem; } else { itemGroup[group] = [element]; } return itemGroup; }); void _dispose(BuildContext context, ChangeNotifier? notifier) { notifier?.dispose(); _scrollController.dispose(); } void _scrollListener(StickyGroupListViewModel viewModel) { // ヘッダーの接触判定を行う(後述) } |
今回作成するスティッキーヘッダーでは、ScrollControllerを使ってヘッダー同士の接触を感知し、ヘッダー自身の高さと表示非表示を調整するように実装していきます。
build の中身を見ていきましょう。
itemGroups ではイニシャライザで受け取った element と groupBy を使い、グループ化を行っていきます。
次の itemGroups.keys.forEach では、さきほど取得した itemGroups のkeyごとにGlobalObjectKeyを生成し、 _headerGlobalKeys にまとめていますね。
GlobalObjectKeyはヘッダーのスクロール位置を取得するのと、ヘッダーごとの高さを管理する為に利用します。
return ではListenableProviderを返しています。
Providerを使う場面ではほとんどChangeNotifierProviderだと思いますが、 _scrollController.dispose() を行うために、Providerのdispose処理を設定できるこちらを使用しています。
create: ではViewModelの生成と _scrollController.addListener() などの初期化処理を行っていますね。
最後の child: では、リストの本体となる_SliverGroupListを入れています。
これによって、ViewModelでヘッダーごとの設定値を管理し、変更されるごとに_SliverGroupListを再ビルドする形になります。
次にリストの本体となる_SliverGroupListを実装していきましょう。
まずは変数とイニシャライザからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class _SliverGroupList<T, E extends Object> extends StatelessWidget { final Widget Function(E value) groupHeaderTitleBuilder; final Widget Function(BuildContext context, T element) itemBuilder; final Future<void> Function()? onRefresh; final Color? groupHeaderBackgroundColor; final Map<E, List<T>> itemGroups; final Map<E, GlobalObjectKey> headerGlobalKeys; final ScrollController? scrollController; _SliverGroupList( {required this.groupHeaderTitleBuilder, required this.itemBuilder, required this.onRefresh, required this.groupHeaderBackgroundColor, required this.itemGroups, required this.headerGlobalKeys, required this.scrollController}); } |
ほとんどがStickyGroupListがイニシャライザで受けとったやつですね。
次に build 周りを書いていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @override Widget build(BuildContext context) { final viewModel = context.watch<StickyGroupListViewModel>(); return sliverGroupListWidget(viewModel); } Widget sliverGroupListWidget(StickyGroupListViewModel viewModel) { List<Widget> slivers = []; itemGroups.forEach((key, value) { final globalKey = headerGlobalKeys[key]; slivers ..add(DefaultSliverPersistentHeader( headerKey: globalKey, title: groupHeaderTitleBuilder(key), backgroundColor: groupHeaderBackgroundColor, pinned: viewModel.pinnedMap[globalKey], height: viewModel.groupHeaderHeightMap[globalKey], )) ..add(SliverList( delegate: SliverChildBuilderDelegate( (context, index) => itemBuilder(context, value[index]), childCount: value.length))); }); final onRefresh = this.onRefresh; if (onRefresh != null) { return PlatformRefreshIndicator( onRefresh: onRefresh, slivers: slivers, controller: scrollController); } else { return CustomScrollView( slivers: slivers, controller: scrollController, ); } } |
最初の final viewModel = context.watch<StickyGroupListViewModel>() によって、ViewModelからの変更通知で再buildを行なってくれるようになります。
Widget sliverGroupListWidget(StickyGroupListViewModel viewModel) を見ていきましょう。
まずはリストの中身である slivers の生成を行なっていきます。
今回リスト表示に利用するCustomScrollViewでは、要素ごとにヘッダーやリストを配列として持つ必要があります。
なので受け取ったModelをグループ化した itemGroup からkey(グループID)とvalue(Model本体)をforEachで取得し、ヘッダーとヘッダーに紐づくリストをそれぞれ生成していますね。
またここで、さきほど生成したGlobalObjectKeyをヘッダーに渡しています。
渡した先でヘッダーとなるWidgetにGlobalObjectKeyを設定してあげることで、StickyGroupList上でヘッダーごとにスクロール位置を取得することができるようになります。
次の onRefresh でのif文では、リストを下に引っ張った時の更新処理が設定されるかどうかで分岐しています。
CustomScrollViewで上記処理を実装したい場合、MaterialではRefreshIndicatorを利用する必要があります。
PlatformRefreshIndicatorとDefaultSliverPersistentHeaderは自作したクラスなので、次はこちらを実装していきましょう。
Material(Android)とCupertino(iOS)デザインへの対応
今回作成するアプリは2つのデザインに対応していますが、この対応のほとんどがpodspecで記載したflutter_platform_widgetsを使えば解決します。
しかし一部対応していないwidgetがあるので、それに関しては自作する必要があります。
今回はRefreshIndicatorを各デザインに対応させるため、PlatformRefreshIndicatorを実装していきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; class PlatformRefreshIndicator extends PlatformWidget { final Key? key; final Future<void> Function() onRefresh; final List<Widget> slivers; final ScrollController? controller; PlatformRefreshIndicator( {this.key, required this.onRefresh, required this.slivers, this.controller}); @override Widget build(BuildContext context) { return PlatformWidget( material: (_, __) => RefreshIndicator( child: CustomScrollView( key: key, controller: controller, slivers: slivers, ), onRefresh: onRefresh), cupertino: (_, __) => CustomScrollView( key: key, controller: controller, slivers: <Widget>[ CupertinoSliverRefreshControl( refreshTriggerPullDistance: 100.0, refreshIndicatorExtent: 60.0, onRefresh: onRefresh, ), ] + slivers, ), ); } } |
build ではflutter_platform_widgetsのPlatformWidgetを返していますね。
これによってAndroidの場合は material: のwidgetが、iOSの場合は cupertino: のwidgetが表示されるようになります。
少々面倒な感じになっていますが、MaterialとCupertinoでリストを下に引っ張った時の実装方法が少し異なるため、このように分ける必要があります。
MaterialではRefreshIndicatorの child: にCustomScrollViewを、CupertinoではCupertinoSliverRefreshControlを slivers: 配列の一番上に配置しましょう。
これで更新処理時のインジケータがそれぞれのデザインで表示されるようになりました。
SliverPersistentHeaderの実装
CustomScrollViewでスティッキーヘッダーを表示したい場合、冒頭にもあったSliverPersistentHeaderを利用するのがおそらく一番の近道です。
イニシャライザにある pinned: にtrueを設定することで、設定したヘッダーに紐づくリストがスクロールしきった後でも、画面上部に残り続けるようになります。
この特性を利用して、全てのヘッダーの pinned: をtrueにした上で、それぞれのヘッダーの高さを変えていくように実装していきましょう。
また、ヘッダーのデザインは自由にカスタムはできるのですが、今回はtitle部分のみをwidgetとして設定できるようにしたものを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class DefaultSliverPersistentHeader extends StatelessWidget { final Key? headerKey; final Widget? title; final Color? backgroundColor; final double maxHeight; final double minHeight; DefaultSliverPersistentHeader( {this.headerKey, this.title, this.backgroundColor, required this.maxHeight, required this.minHeight}); @override Widget build(BuildContext context) { return SliverPersistentHeader( key: headerKey, delegate: _DefaultSliverPersistentHeaderDelegate( child: Container( color: backgroundColor, child: Padding( child: Align( alignment: Alignment.centerLeft, child: title, ), padding: EdgeInsets.symmetric(horizontal: 8), )), maxHeight: maxHeight, minHeight: minHeight), pinned: true); } } |
build から見ていきましょう。
key: には受け取ったGlobalObjectKeyを入れることで、親WidgetでSliverPersistentHeaderのスクロール位置の情報などが取得可能になります。
delegate: ではSliverPersistentHeaderの中身を定義していきます。
ただしそれにはdelegateクラスを経由する必要があるので、次は_DefaultSliverPersistentHeaderDelegateを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class _DefaultSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget child; final double maxHeight; final double minHeight; _DefaultSliverPersistentHeaderDelegate({required this.child, required this.maxHeight, required this.minHeight}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child; @override double get maxExtent => maxHeight; @override double get minExtent => minHeight; @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => minExtent != oldDelegate.minExtent || maxExtent != oldDelegate.maxExtent } |
SliverPersistentHeaderのdelegateなので、そのままSliverPersistentHeaderDelegateを継承しています。
build にはStatelessWidgetなどと同じように、表示するWidgetを設定しましょう。
maxExtent と minExtent はスクロール時のヘッダーの高さの最大値と最低値を設定できます。
minExtent に_SliverGroupListから受け取った縮小した状態のヘッダーの高さを設定することで、スティッキーヘッダーのような動作を再現することができます。
maxExtent にはヘッダーのデフォルトの高さを渡しましょう。
一見すると、 minExtent に0を設定すればスティッキーヘッダーになりそうですが、ヘッダーが接触する前にヘッダーの高さが0になってしまうので、こちら側で管理して使ってあげる必要があります。
shouldRebuild では、今回の実装だと maxExtent と minExtent が可変なので、それらの値が前と異なる場合は再buildされるようにtrueを返しています。
これでWidget関連の実装はできたので、今度はヘッダーの高さを管理するStickyGroupListViewModelを実装していきましょう。
ヘッダーごとの高さを管理する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class StickyGroupListViewModel extends ChangeNotifier { final double headerDefaultHeight; final List<GlobalObjectKey> keys; Map<GlobalObjectKey, double> _headerHeightMap = {}; Map<GlobalObjectKey, double> get headerHeightMap => _headerHeightMap; StickyGroupListViewModel({required this.headerDefaultHeight, required this.keys}) { _headerHeightMap = keys.fold(Map<GlobalObjectKey, double>(), (previousValue, element) { previousValue[element] = headerDefaultHeight; return previousValue; }); } // 指定したkeyのヘッダーの高さを取得する double getHeaderHeight(GlobalObjectKey? key) { return _headerHeightMap[key] ?? 0; } // 指定したkeyのヘッダーの高さを更新する void setHeaderHeight(GlobalObjectKey key, double height) { if (height > headerDefaultHeight) { if (getHeaderHeight(key) == headerDefaultHeight) { return; } _headerHeightMap.update(key, (value) => headerDefaultHeight); } else if (height < 0) { if (getHeaderHeight(key) == 0) { return; } _headerHeightMap.update(key, (value) => 0); } else { if (getHeaderHeight(key) == height) { return; } _headerHeightMap.update(key, (value) => height); } notifyListeners(); } } |
ViewModelではChangeNotifierを継承し、 notifyListeners() で値の変更をWidgetに通知しましょう。
メンバ変数の _headerHeightMap はGlobalObjectKeyをkeyとして、ヘッダーごとの高さの値が保持されています。
setHeaderHeight では、ヘッダーの高さの範囲である0〜デフォルト値に納まるように制御を入れていますね。
これでヘッダーを更新するための土台ができたので、最後にスクロール時にヘッダー同士が接触した場合の挙動を実装していきましょう。
ヘッダー同士の接触を検知し、ヘッダーの高さを操作する
StickyGroupListで未実装だった_scrollListenerの中身を実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void _scrollListener(StickyGroupListViewModel viewModel) { final keyList = _headerGlobalKeys.values.toList(); final currentKey = keyList.firstWhere((element) => viewModel.getHeaderHeight(element) > 0); final currentHeader = currentKey.currentContext?.findRenderObject() as RenderSliverPersistentHeader?; if (currentHeader == null) { return; } final isDownScroll = currentHeader.constraints.userScrollDirection == ScrollDirection.reverse; if (isDownScroll && keyList.length > keyList.indexOf(currentKey) + 1) { // 下スクロール時 } else if (!isDownScroll) { // 上スクロール時 } } |
まず最初に、表示されている中で一番上のヘッダーである currentHeader を取得します。
表示されている中ということなので、 _headerGlobalKeys を最初から順番に見て高さが0以上のものを currentHeader として取得していますね。
次の final isDownScroll = currentHeader.constraints.userScrollDirection == ScrollDirection.reverse では、上のスクロールか下のスクロールかを取得しています。
こちらは ScrollDirection.reverse で下スクロール、 ScrollDirection.forward で上スクロールと判定することができます。
それではまず下スクロールであった場合の処理を書いていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 下スクロール時 // ヘッダー同士が接触した後の高さ更新 if (keyList.length > keyList.indexOf(currentKey) + 1) { final nextKey = keyList[keyList.indexOf(currentKey) + 1]; final nextHeader = nextKey.currentContext?.findRenderObject() as RenderSliverPersistentHeader?; if (nextHeader != null) { final double currentHeight = nextHeader.constraints.precedingScrollExtent - _scrollController.offset; // 高さ更新 viewModel.setHeaderHeight(currentKey, currentHeight); } } |
下スクロールでは上部のヘッダーと、その次のヘッダーを見て上部のヘッダーの高さを調整していきます。
currentHeader を取得した時と同じ要領で1つ下のヘッダーである nextHeader を取得し、 nextHeader.constraints.precedingScrollExtent - _scrollController.offset で、下部のヘッダーが上部のヘッダーに食い込んだ状態での、上部ヘッダーのあるべきサイズである currentHeight を計算しています。
nextHeader.constraints.precedingScrollExtent は、 nextHeader の座標がスクロールの中でどの位置にあるかという値になっています。
そこからスクロール位置である _scrollController.offset をマイナスすることで、画面上の最上部から nextHeader までの間がどれだけ空いているかを取得しているという感じです。
そうしたら currentHeight をViewModelに渡しましょう。
これで下スクロール時に上部のヘッダーの高さが計算され、ViewModelを通じてヘッダーの高さが変更されるようになりました。
次に上スクロール時の処理を実装していきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 上スクロール時 // ヘッダー同士が接触した後の高さ更新 if (viewModel.getHeaderHeight(currentKey) < groupHeaderHeight) { // 上部ヘッダーの高さを変更 if (keyList.length > keyList.indexOf(currentKey) + 1) { final nextKey = keyList[keyList.indexOf(currentKey) + 1]; final nextHeader = nextKey.currentContext?.findRenderObject() as RenderSliverPersistentHeader?; if (nextHeader != null) { final currentHeight = nextHeader.constraints.precedingScrollExtent - _scrollController.offset; // 高さ更新 viewModel.setHeaderHeight(currentKey, currentHeight); } } } else if (0 <= keyList.indexOf(currentKey) - 1) { // 上部ヘッダーより前にあるヘッダーを表示 final previousKey = keyList[keyList.indexOf(currentKey) - 1]; final previousHeight = currentHeader.constraints.precedingScrollExtent - _scrollController.offset; // 高さ更新 viewModel.setHeaderHeight(previousKey, previousHeight); } |
こちらは下スクロールと異なり、処理が2つに分けれています。
上側は下スクロールと同じく、ヘッダー同士が接触した時の高さを計算しており、計算方法なども同じですね。
下側は最上部にヘッダーがある状態で、上スクロールした時にさらにその上にあるヘッダーを表示するかどうかを計算しています。
ただ計算方法などはほとんど同じで、 nextHeader ではなく、最上部に表示されている currentHeader を使っているだけですね。
分岐条件としても、上側は最上部のヘッダーの高さが最大値になっていない場合で、下側は最上部のヘッダーが最大値の時に一つ前のヘッダーが存在するかを見ているだけです。
ヘッダーの高さが最小値である0になっていないかという条件も必要ではありますが、 currentHeader 取得時点で高さが0より上になるようになっているので省いています。
これで上下のスクロールで上部ヘッダーの高さの変更をViewModelから、DefaultSliverPersistentHeaderまで通知されるようになりました。
スティッキーヘッダーを使ってみる
スティッキーヘッダーを表示できるクラスが実装できたので、実際にアプリ上で表示させてみましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'platform/platform_theme.dart'; import 'sticky_group_list.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return PlatformApp( title: 'Sliver Group List', material: (context, platform) => MaterialAppData(theme: ThemeData(primarySwatch: Colors.blue)), cupertino: (context, platform) => CupertinoAppData(theme: CupertinoThemeData(primaryColor: CupertinoColors.systemBlue)), home: _HomeView(), ); } } class _HomeView extends StatelessWidget { @override Widget build(BuildContext context) { final list = List.generate(100, (index) => _ListData(id: index, title: 'Title ${index.toString()}')); return PlatformScaffold( appBar: PlatformAppBar( title: PlatformText('サンプル'), ), body: StickyGroupList( elements: list, groupBy: (_ListData element) => element.id.toString(), groupSeparatorTitleBuilder: (String groupByValue) => PlatformText(groupByValue), itemBuilder: (context, _ListData element) => Container( margin: EdgeInsets.all(4), padding: EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4), ), child: PlatformText(element.title, style: TextStyle(color: Colors.white, fontSize: 18)), ), onRefresh: () async { print("refresh"); }, groupSeparatorBackgroundColor: PlatformTheme.of(context).primaryColor, ), iosContentPadding: true, iosContentBottomPadding: true, ); } } class _ListData { final int id; final String title; _ListData({required this.id, required this.title}); } |
いくつかflutter_platform_widgetsのクラスを利用しています。
Material -> PlatformApp
Scaffold -> PlatformScaffold
Text -> PlatformText
Theme の primaryColor については、flutter_platform_widgetsには実装されていなかったので以下のように自作しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class PlatformTheme { final BuildContext context; PlatformTheme({required this.context}); factory PlatformTheme.of(BuildContext context) => PlatformTheme(context: context); Color get primaryColor { if (Platform.isIOS) { return CupertinoTheme.of(context).primaryColor; } else { return Theme.of(context).primaryColor; } } } |
実行されている端末がiOSかどうかを見て、そうだった場合は CupertinoTheme を、そうじゃなかった場合は Theme で primaryColor を返しているだけですね。
iOSとAndroidそれぞれで以下のようになります!
以上がスティッキーヘッダーとグループ化されたリスト(MaterialとCupertino両対応)の実装でした。
作成したアプリは以下にあるので、こちらも参考にしてみてください。
https://github.com/ryuto-imai/flutter_sticky_group_list
書いた人はこんな人

IT技術10月 27, 2023Jiraの自動化
IT技術8月 16, 2023Lighthouseで計測したパフォーマンススコアのばらつきを減らす方法
IT技術11月 7, 2022Ruby on Rails&GraphQLのエラーレスポンス
IT技術7月 13, 2022Ruby on Rails & GraphQLの環境構築と実装