動くものを作りたい現役アプリエンジニアのブログ

Engineer Life Blog

FlutterのListViewにお気に入り登録機能と画面遷移機能を導入してみる

ListViewで一覧は作れた。でも一覧をカスタマイズしてお気に入り登録と画面遷移機能を入れてみたいけどどうすればいい?

こんな課題に答えていきます。

前提として下記記事の英単語一覧まではできていることを前提に説明していきます。

 

ほぼ全てのモバイルアプリケーションに必須なListVIewを使った一覧を作りました。

この一覧を使って自分のお気に入りの英単語をお気に入り登録する機能を作ります。

ListViewにお気に入り登録マークを作る

まずはお気に入りマークを作っていきます。

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      home: Scaffold(
        body: Center(
          child: RandomWords(),
        ),
      ),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();//追加
  final TextStyle _biggerFont = const TextStyle(fontSize: 18);
  @override
  Widget build(BuildContext context) {
    return Scaffold (
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        padding: const EdgeInsets.all(16),
        itemBuilder: (BuildContext _context, int i) {
          if (i.isOdd) {
            return Divider();
          }
          final int index = i ~/ 2;
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);//追加
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: Icon(//追加
        alreadySaved ? Icons.favorite : Icons.favorite_border,//追加
        color: alreadySaved ? Colors.red : null,//追加
      ),
    );
  }
}

ハートマークを表示しました。まだ、押すことはできません。見た目だけです。

またsaveと命名している変数をいくつか設定していますが、これは次の段階でお気に入り登録ボタンを押された時にその情報を保存するための変数です。

 

ListViewに追加したお気に入りマークを動的に動かす(押せるようにする)

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      home: Scaffold(
        body: Center(
          child: RandomWords(),
        ),
      ),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();
  final TextStyle _biggerFont = const TextStyle(fontSize: 18);
  @override
  Widget build(BuildContext context) {
    return Scaffold (
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        padding: const EdgeInsets.all(16),
        itemBuilder: (BuildContext _context, int i) {
          if (i.isOdd) {
            return Divider();
          }
          final int index = i ~/ 2;
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {//追加
        setState(() {//追加
          if (alreadySaved) {//追加
            _saved.remove(pair);//追加
          } else {//追加
            _saved.add(pair);//追加
          }//追加
        });//追加
      },//追加
    );
  }
}

先ほど追加した英単語をクリックするとハートのマークが赤色に変わるようになりました。

_buildRowメソッドでは新たにonTapというイベントハンドラのようなものを追加しました。

クリックもしくはタッチした時にonTapが動作します。

setState()はStatefulWidgetに登録されているstate(端末上で状態を持っている)パラメータ に変更したことを通知するための仕組みです。通知することでstateが変化したことをStatefulWidgetが検知し、リアルタイムに画面が変化します

setState()の中では単語がすでにハートがマークされている状態(お気に入り登録されている状態)がどうかをif文で比較し、すでにマークされているならマークを外し、逆にまだマークされていないならマークすると言う処理です。一言で言うとトグル処理を実装しています。

 

ListViewを使ってお気に入り画面への画面遷移を実装

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      home: Scaffold(
        body: Center(
          child: RandomWords(),
        ),
      ),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();
  final TextStyle _biggerFont = const TextStyle(fontSize: 18);

  void _pushSaved() {//追加ここから
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final tiles = _saved.map(
            (WordPair pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();
          return Scaffold(
            appBar: AppBar(
              title: Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    ); //ここまで追加
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold (
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [//追加
          IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),//追加
        ],//追加
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        padding: const EdgeInsets.all(16),
        itemBuilder: (BuildContext _context, int i) {
          if (i.isOdd) {
            return Divider();
          }
          final int index = i ~/ 2;
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }
}

IconButton(icon: Icon(Icons.list), onPressed: _pushSaved)まずは英単語一覧が出ている画面の上部ヘッダーappbarの右側にお気に入りのみを表示する画面に遷移するボタンを表示します。

onPressed:はjavascriptのイベントハンドラのイメージでonClickのような意味合いと同じものです。_pushSaved関数を呼び出します。

_pushSaved()の中の動作

Navigator.of(context).push()は別の画面に遷移する際に使われるもので、次画面に渡す情報をstateに入れるようなイメージです。

逆に戻る時は.pop()が使われます。

MaterialPageRouteは遷移先の画面を定義するWigetです。この中で画面の体裁を作っていきます。

final tiles = _saved.map()は1つ前の英単語一覧画面でハートマークを押して保存していた情報を引っ張り出してきています。

final divided = ListTile.divideTiles().toList()では遷移後の画面のbody部分のレイアウトや表示する中身をList型に変換しています。

これで遷移後の画面定義や中身の設定をして遷移後の画面を作って完成です。

実行するとこのようになります。

 

所感

Flutterやはりイメージ通りに実装できます。

Navigator.of(context).push()など知らないと実装できなさそうな作りもありますが、公式やインターネット上にも情報があるのですぐに見つけられると思います。

また、ここでは画面遷移後の画面もmain.dartで実装していますが、実際に開発する時は別ファイルに分けてそちらで作り込むのがいいと思います。

 

ここまで見ていただきありがとうございました。

Flutter系の記事を上げていこうと思いますので、そちらもぜひみてください!