Flutterで文字を並べて階層化してみる – 開発日記(2)

アイディア・レーン 開発日記アイキャッチ

実装開始 2023/03/17

まずは文字を縦に並べる。

そして階層化出来るようにしてみた。

といってもいきなりこんなにコーディング出来たわけでなく、リリースされたばかりのChatGPTにコードを生成してもらったものだ。

正直あまりflutterもdartも分からない状態で、AIが作ってくれたコードを元になんとか動かしたというところ。

しかしChatGPTが生成するコードを動かそうとすると、ほぼ必ずエラーが出てビルドに失敗する。でもそのエラーを見ても何のことか全くわからないので、ネットでエラーメッセージを調べて適当に直すという感じだった。

今振り返ると、大抵はflutterの仕様変更のせいで、nullチェックやmaterial関連の記述変更が必要というものが多かったです。

今なら null チェックが必要ならこうすれば良いとすぐ分かるのですが……。
当時は大量に出るエラーを一つずつ理解するには時間がかかりすぎたので、とにかくネットの情報を見ながら適当に直していました。


コードはこんな感じ。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(child: Lane()),
      ),
    );
  }
}

class Lane extends StatefulWidget {
  @override
  _LaneState createState() => _LaneState();
}

class _LaneState extends State<Lane> {
  List<Idea> ideas = [Idea(key: GlobalKey<_IdeaState>(), index: 0)];

  void addNewIdea(int currentIndex) {
    setState(() {
      ideas.insert(currentIndex + 1,
          Idea(key: GlobalKey<_IdeaState>(), index: currentIndex + 1));
      for (int i = currentIndex + 2; i < ideas.length; i++) {
        ideas[i].index = i;
      }
    });
  }

  void moveIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      setState(() {
        Idea temp = ideas[currentIndex];
        ideas[currentIndex] = ideas[currentIndex + direction];
        ideas[currentIndex + direction] = temp;
        ideas[currentIndex].key.currentState?.updateIndex(currentIndex);
        ideas[currentIndex + direction]
            .key
            .currentState
            ?.updateIndex(currentIndex + direction);
      });
    }
  }

  //Ideaが削除された後に、一つ上のIdeaにフォーカスが移動するようになっています。もし削除されたIdeaが最初のIdeaだった場合、その下のIdeaにフォーカスが移動します
  void deleteIdea(int currentIndex) {
    if (ideas.length > 1) {
      setState(() {
        ideas.removeAt(currentIndex);
        for (int i = currentIndex; i < ideas.length; i++) {
          ideas[i].index = i;
        }
        if (currentIndex > 0) {
          ideas[currentIndex - 1]
              .key
              .currentState
              ?._textFocusNode
              .requestFocus();
        } else {
          ideas[currentIndex].key.currentState?._textFocusNode.requestFocus();
        }
      });
    }
  }

  void focusAdjacentIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      ideas[currentIndex + direction]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  @override
  Widget build(BuildContext context) {
    return LaneInheritedWidget(
      laneState: this,
      child: ListView.builder(
        itemCount: ideas.length,
        itemBuilder: (context, index) {
          return ideas[index];
        },
      ),
    );
  }
}

class LaneInheritedWidget extends InheritedWidget {
  final _LaneState laneState;

  LaneInheritedWidget({required this.laneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(LaneInheritedWidget old) => true;

  static _LaneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
        .laneState;
  }
}

class Idea extends StatefulWidget {
  int index;
  final GlobalKey<_IdeaState> key;

  Idea({required this.key, required this.index}) : super(key: key);

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

class _IdeaState extends State<Idea> {
  TextEditingController _textController = TextEditingController();
  int _indentLevel = 0;
  FocusNode _textFocusNode = FocusNode();

  void updateIndex(int newIndex) {
    setState(() {
      widget.index = newIndex;
    });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      FocusScope.of(context).requestFocus(_textFocusNode);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
        onKey: (FocusNode node, RawKeyEvent event) {
        if (event.runtimeType == RawKeyDownEvent) {
          if (event.isControlPressed) { //コントロールキー
            if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
              LaneInheritedWidget.of(context).moveIdea(widget.index, -1);
            } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
              LaneInheritedWidget.of(context).moveIdea(widget.index, 1);
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
            LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, -1);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
            LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, 1);
          } else if (event.logicalKey == LogicalKeyboardKey.tab) { //TABキー
            if (event.isShiftPressed) {
              setState(() {
                _indentLevel = max(0, _indentLevel - 1);
              });
            } else {
              setState(() {
                _indentLevel += 1;
              });
            }
            return KeyEventResult.handled;; //デフォルトのタブの挙動を無効化して、フォーカスの移動を防ぐ
          } else if ((event.logicalKey == LogicalKeyboardKey.delete ||
                  event.logicalKey == LogicalKeyboardKey.backspace) &&
              _textController.text.isEmpty) {
            LaneInheritedWidget.of(context).deleteIdea(widget.index);
          }
        }
        return KeyEventResult.ignored;
      },
      child: RawKeyboardListener(
          focusNode: FocusNode(),
      child: Container(
        margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(
              flex: 1,
              child: Container(
                color: Colors.grey[300],
                child: CustomPaint(
                  painter: HeaderCanvas(),
                ),
              ),
            ),
            SizedBox(width: 16),
            Expanded(
              flex: 4,
              child: Container(
                padding: EdgeInsets.only(left: 20.0 * _indentLevel),
                child: TextField(
                  controller: _textController,
                  focusNode: _textFocusNode,
                  maxLines: 1,
                  decoration: InputDecoration(
                    hintText: 'Enter your idea',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.multiline,
                  textInputAction: TextInputAction.done,
                  onSubmitted: (value) {
                    _textFocusNode.unfocus(); // Unfocus the current TextField
                    LaneInheritedWidget.of(context).addNewIdea(widget.index);
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    )
    );
  }
}

class HeaderCanvas extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
// Your custom painting logic goes here
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

アプリのタイトルは、レーンの中に色々書けるものを目指したので in Lane とした。(当時)


※本記事は、過去の記録(メモ)をもとに、他の方が読んで分かるように加筆・補足して掲載しています

テキストベースの思考整理ツール「アイディア・レーン」最新版はこちら

コメント

タイトルとURLをコピーしました