🚛

Stepper classなるものを使ってみた

2023/09/17に公開

Overview

https://api.flutter.dev/flutter/material/Stepper-class.html
公式より引用
一連のステップの進行状況を表示するマテリアル・ステッパー・ウィジェットです。ステッパーは、1つのステップの完了が別のステップの完了を必要とするフォームや、フォーム全体を送信するために複数のステップを完了する必要があるフォームの場合に特に便利です。

ウィジェットは柔軟なラッパーです。親クラスは、このウィジェットが提供する 3 つのコールバックによってトリガーされるロジックに基づいて、currentStep をこのウィジェットに渡す必要があります。

summary

今回は、配達しているトラックがどこにいるのかリアルタイムでわかるように確認できるUIを再現してみました。

必要なパッケージ
日本時間を表示するのに使用
https://pub.dev/packages/intl
状態管理はこれが流行りですね
https://pub.dev/packages/flutter_riverpod
他にも使用するパッケージあるので、ドキュメント通りに追加してください。使ってる人は知ってると思う。
https://pub.dev/packages/freezed
Firestoreを使うので、以下の2個のパッケージを追加する。
https://pub.dev/packages/firebase_core
https://pub.dev/packages/cloud_firestore

モデルを作る。Freezedでは、Timestampのデータ型がないので、コンバーターを自作する必要がある。

// tracker_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'tracker.freezed.dart';
part 'tracker.g.dart';

// ファイルを自動生成するコマンド
// flutter pub run build_runner build --delete-conflicting-outputs

// タイムスタンプをDateTime型に変換するコンバーター
class TimestampConverter implements JsonConverter<DateTime?, Timestamp?> {
  const TimestampConverter();

  
  DateTime? fromJson(Timestamp? json) => json?.toDate();

  
  Timestamp? toJson(DateTime? object) =>
      object == null ? null : Timestamp.fromDate(object);
}

// Freezedで作成するモデルクラス

class Tracker with _$Tracker {
  const factory Tracker({
    ('') String location,
    () DateTime? createdAt,
  }) = _Tracker;

  factory Tracker.fromJson(Map<String, dynamic> json) =>
      _$TrackerFromJson(json);
}

モデルを定義したら、自動生成のコマンドを実行する。

flutter pub run build_runner build --delete-conflicting-outputs

自動生成されたファイルでエラーが出るものがあるが、timestampをimportすればエラーは消えるので対応する。

UIを作成する。とりあえずこの通りに作ってくれれば、timelineのようなUIを作成できる。丸と棒線が繋がっている画面ですね。
timelinesという簡単にタイムラインのUIを作成できるパッケージがあるのですが、そちらは2年間メンテナンスされてないので、使うのはお勧めしません。

ボタンを押すと、画面が切り替わるロジックを作るにはConsumerStatefulWidgetが必要なので、StatefulWidgetを使っている状態になってます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:ore_chans_app/firebase/src/features/post_crud_app/application/post_provider.dart';
import 'package:ore_chans_app/widget_cookbook/ui/stepper/model/tracker.dart';

// Firestoreのtrackerコレクションをリアルタイムに取得するProvider
final trackStreamProvider = StreamProvider.autoDispose<List<Tracker>>((ref) {
  final snapshot = ref.watch(fireStoreProvider).collection('tracker').snapshots();
  return snapshot.map((query) => query.docs.map((doc) {
    final data = doc.data();
    return Tracker.fromJson(data);
  }).toList());
});

class TrackerPage extends ConsumerStatefulWidget {
  const TrackerPage({super.key});

  
  ConsumerState<ConsumerStatefulWidget> createState() => _TrackerPageState();
}

class _TrackerPageState extends ConsumerState<TrackerPage> {
  int _currentStep = 0;

  
  Widget build(BuildContext context) {
    final trackers = ref.watch(trackStreamProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('トラックの現在地 Stf')),
      body: trackers.when(
        data: (trackersList) {
          return Stepper(
            currentStep: _currentStep,
            onStepContinue: () {
              if (_currentStep < trackersList.length - 1) {
                setState(() {
                  _currentStep += 1;
                });
              }
            },
            onStepCancel: () {
              if (_currentStep > 0) {
                setState(() {
                  _currentStep -= 1;
                });
              }
            },
            steps: trackersList.map((tracker) => Step(
                  title: Column(
                    children: [
                      Text(tracker.location),// 現在地を表示
                      Text(DateFormat('yyyy/MM/dd HH:mm').format(tracker.createdAt!)),// 到着時刻を表示
                    ],
                  ),
                  content: Container(),
                )).toList(),
          );
        },
        error: (_, __) => const Center(child: Text('エラーが発生しました')),
        loading: () => const Center(child: CircularProgressIndicator()),
      ),
    );
  }
}

main.dartでこんな感じでいつものように、importすれば動くはずです。

Future<void> main() async {
    WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LINE Messenger API',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TrackerPage(),
    );
  }
}

Firestoreには、ダミーのデータを入れおきましょう。


こんな感じでUIに表示されます

thoughts

今回は、timelinesパッケージがなくても同じようなタイムラインのUIを作るのを再現してみましたが、データを追加するたびに、エラーが出てきちゃう問題後で出てきました😇

例外が発生しました
_AssertionError ('package:flutter/src/material/stepper.dart': Failed assertion: line 369 pos 12: 'widget.steps.length == oldWidget.steps.length': is not true.)

もしかしたら、標準のWidgetだと限界があるのかもしれないですね。

これが昔気に入ってた、timelinesです。
https://pub.dev/packages/timelines

Discussion