見出し画像

TWSNMP開発日誌:モバイル版のノードリストの表示ができた

今朝は3時半から開発開始です。昨日までの作ったノードのデータを保存する仕組みが使えるかどうか試してみたかったので早く目が覚めました。
内部のデータを扱う仕組みとして採用したRiverpodの公式ページ

をもう一度見直してみると作ったものの使い方が間違っていることに気づきました。Flutterだけの使い方とFlutter Hooksと組み合わせた使い方があり、参考にしたサイトから写経したものはFlutter Hooksのほうだったようです。
詳しくは理解できていませんが、説明やサンプルコードを読みとFlutterだけの使い方のほうがよさそいなので書き換えました。

ノードのデータを扱うソースコードは、

import 'dart:convert';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Node {
  String name;
  String ip;
  String snmpMode;
  String community;
  String user;
  String password;
  String icon;

  Node({
    this.name = '',
    this.ip = '',
    this.snmpMode = '',
    this.community = '',
    this.user = '',
    this.password = '',
    this.icon = '',
  });

  Map toMap() => {
        'name': name,
        'ip': ip,
        'snmpMode': snmpMode,
        'community': community,
        'user': user,
        'password': password,
        'icon': icon,
      };

  Node.fromMap(Map map)
      : name = map['name'],
        ip = map['ip'],
        snmpMode = map['snmpMode'],
        community = map['community'],
        user = map['user'],
        password = map['password'],
        icon = map['icon'];
}

final nodesProvider = Provider((_) => Nodes());

class Nodes {
  List<Node> nodes = [];

  Nodes() {
    load();
  }

  bool add(Node n) {
    for (var i = 0; i < nodes.length; i++) {
      if (n.ip == nodes[i].ip) {
        return false;
      }
    }
    nodes.add(n);
    return true;
  }

  bool update(int i, Node n) {
    if (i < 0 || i >= nodes.length) {
      return false;
    }
    nodes[i] = n;
    return true;
  }

  bool delete(int i) {
    if (i < 0 || i >= nodes.length) {
      return false;
    }
    nodes.removeAt(i);
    return true;
  }

  Node get(int i) {
    if (i < 0 || i >= nodes.length) {
      return Node();
    }
    return nodes[i];
  }

  Future save() async {
    if (Platform.operatingSystem == 'macos') {
      return;
    }
    List<String> strNodes = nodes.map((n) => json.encode(n.toMap())).toList();
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setStringList('nodes', strNodes);
  }

  Future load() async {
    if (Platform.operatingSystem == 'macos') {
      return;
    }
    SharedPreferences prefs = await SharedPreferences.getInstance();
    var result = prefs.getStringList('nodes');
    if (result != null) {
      nodes = result.map((f) => Node.fromMap(json.decode(f))).toList();
    }
  }
}

のようになりました。
このテストは、

import 'package:flutter/material.dart';
import 'package:test/test.dart';
import 'package:twsnmpfm/node.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final nodes = Nodes();
  group('Nodes', () {
    test('nodes add', () {
      expect(
          nodes.add(Node(
            name: "node 1",
            ip: "192.168.1.1",
            community: "public",
            snmpMode: "v2c",
          )),
          true);
    });

    test('nodes update', () {
      expect(
          nodes.update(
              0,
              Node(
                  name: "node 1",
                  ip: "192.168.1.1",
                  community: "public",
                  snmpMode: "v2c")),
          true);
    });

    test('nodes delete', () {
      expect(nodes.delete(0), true);
    });
  });
}

のような感じです。テストを実行する時に例外が発生する問題は、

Future load() async {
    if (Platform.operatingSystem == 'macos') {
      return;
    }

のように回避しています。これでよいのか心配ですが。
これらの変更は、

です。
次にノードのデータを表示する部分を作ってみました。データの構造とか心配なので、
表示は、Flutterのリスト

を使うことにしました。

です。先頭にアイコン、タイトルが名前、サブタイトルがIPアドレス、最後にボタンのような感じです。

ノードリスト

<+>ボタンをクリックするダミーのノードを追加するようになっています。動きは、

ノード追加

のような感じです。
最初<+>ボタンをクリックしてもリストが更新されませんでした。
Reverpodのサンプルコード

を参考にしてノードのリストが変更状態をもつProviderを追加して解決できました。

import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:twsnmpfm/node.dart';

final nodeListCountProvider = StateProvider((ref) => 0);

class NodeListPage extends ConsumerWidget {
  const NodeListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final nodes = ref.read(nodesProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('TWSNMP For Mobile')),
      body: Consumer(builder: (context, ref, _) {
        ref.watch(nodeListCountProvider.state).state;
        return Scrollbar(
          child: ListView(
            restorationId: 'node_list_view',
            padding: const EdgeInsets.symmetric(vertical: 8),
            children: [
              for (int i = 0; i < nodes.nodes.length; i++)
                ListTile(
                  leading: getIcon(nodes.nodes[i].icon),
                  title: Text(nodes.nodes[i].name),
                  subtitle: Text(nodes.nodes[i].ip),
                  trailing: const Icon(Icons.more_vert),
                ),
            ],
          ),
        );
      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () => {addNode(ref)},
        child: const Icon(Icons.add),
      ),
    );
  }

  Icon getIcon(icon) {
    switch (icon) {
      case 'laptop':
        return const Icon(Icons.laptop);
      case 'desktop':
        return const Icon(Icons.desktop_windows);
      case 'server':
        return const Icon(Icons.dns);
      case 'cloud':
        return const Icon(Icons.cloud);
    }
    return const Icon(Icons.lan);
  }

  void addNode(WidgetRef ref) {
    final List<String> icons = ["laptop", "dektop", "server", "cloud", "lan"];
    final nodes = ref.read(nodesProvider);
    ref.read(nodeListCountProvider.state).state++;
    final ip = ref.read(nodeListCountProvider.state).state;
    nodes.add(Node(ip: '10.30.1.$ip', name: "node-$ip", icon: icons[ip % 5]));
    print(ip);
  }
}

のソースコードのnodeListCountProviderを使っている部分です。
ノードの編集画面も作りたいところですが、今朝は時間切れです。

明日から

に出かける予定なので今週末の開発はお休みです。

開発は来週に続く


開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。