Flutter에서 "MVM·Provider·SQLite"를 사용하여 "검색창 애플리케이션"만들기

만들 응용 프로그램


는 검색 표시줄에 입력한 키워드를 사용하여 의사록 제목을 검색하는 응용 프로그램입니다.
검색 외에도 로그인, 편집, 필기를 삭제할 수 있다.
screen_shot_01
비고 일람 화면
screen_shot_02
검색 표시줄을 사용하여 제목 검색
screen_shot_03
설명 등록, 편집, 삭제 화면

이루어지다

  • Flutter: 2.0.6
  • Dart: 2.12.3
  • 패키지 구조
    ├── lib
    │   ├── main.dart
    │   ├── model
    │   │   ├── db
    │   │   │   └── app_database.dart
    │   │   ├── entity
    │   │   │   └── memo.dart
    │   │   └── repository
    │   │       └── memo_repository.dart
    │   └── ui
    │       ├── memo_detail (メモ登録・編集・削除画面)
    │       │   ├── memo_detail.dart
    │       │   └── memo_detail_view_model.dart
    │       └── memo_list (メモ一覧画面)
    │           ├── memo_list.dart
    │           └── memo_list_view_model.dart
    ├── pubspec.yaml
    
    먼저 pubspec.yaml에 프로그램 라이브러리를 추가합니다.
    pubspec.yaml
    dependencies:
      flutter:
        sdk: flutter
      # 追加: memo.dartの「DateFormat」で使用する
      flutter_localizations:
        sdk: flutter
    
      # 以下を追加
      sqflite: ^2.0.0+3
      # app_database.dartの「join」で使用する
      path: 1.8.0
      provider: ^5.0.0
      # Memoのid生成で使用する
      uuid: ^3.0.4
    

    model


    모델 포장을 제작한app_database.dart,memo.dart,memo_repository.dart.
    app_database.dart
    import 'package:path/path.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    import 'package:sqflite/sqflite.dart';
    
    class AppDatabase {
      final String _tableName = 'Memo';
      final String _columnId = 'id';
      final String _columnTitle = 'title';
      final String _columnContent = 'content';
      final String _columnCreatedAt = 'created_at';
    
      Database _database;
    
      Future<Database> get database async {
        if (_database != null) return _database;
        _database = await _initDB();
        return _database;
      }
    
      Future<Database> _initDB() async {
        String path = join(await getDatabasesPath(), 'memo.db');
    
        return await openDatabase(
          path,
          version: 1,
          onCreate: _createTable,
        );
      }
    
      Future<void> _createTable(Database db, int version) async {
        String sql = '''
          CREATE TABLE $_tableName(
            $_columnId TEXT PRIMARY KEY,
            $_columnTitle TEXT,
            $_columnContent TEXT,
            $_columnCreatedAt TEXT
          )
        ''';
    
        return await db.execute(sql);
      }
    
      Future<List<Memo>> loadAllMemo() async {
        final db = await database;
        var maps = await db.query(
          _tableName,
          orderBy: '$_columnCreatedAt DESC',
        );
    
        if (maps.isEmpty) return [];
    
        return maps.map((map) => fromMap(map)).toList();
      }
    
      Future<List<Memo>> search(String keyword) async {
        final db = await database;
        var maps = await db.query(
          _tableName,
          orderBy: '$_columnCreatedAt DESC',
          where: '$_columnTitle LIKE ?',
          whereArgs: ['%$keyword%'],
        );
    
        if (maps.isEmpty) return [];
    
        return maps.map((map) => fromMap(map)).toList();
      }
    
      Future insert(Memo memo) async {
        final db = await database;
        return await db.insert(_tableName, toMap(memo));
      }
    
      Future update(Memo memo) async {
        final db = await database;
        return await db.update(
          _tableName,
          toMap(memo),
          where: '$_columnId = ?',
          whereArgs: [memo.id],
        );
      }
    
      Future delete(Memo memo) async {
        final db = await database;
        return await db.delete(
          _tableName,
          where: '$_columnId = ?',
          whereArgs: [memo.id],
        );
      }
    
      Map<String, dynamic> toMap(Memo memo) {
        return {
          _columnId: memo.id,
          _columnTitle: memo.title,
          _columnContent: memo.content,
          _columnCreatedAt: memo.createdAt.toUtc().toIso8601String()
        };
      }
    
      Memo fromMap(Map<String, dynamic> json) {
        return Memo(
          id: json[_columnId],
          title: json[_columnTitle],
          content: json[_columnContent],
          createdAt: DateTime.parse(json[_columnCreatedAt]).toLocal(),
        );
      }
    }
    
    memo.dart
    import 'package:intl/intl.dart';
    
    class Memo {
      String id;
      String title;
      String content;
      DateTime createdAt;
    
      String getContent() {
        String cont = content.replaceAll('\n', ' ');
        if (cont.length <= 10) return cont;
        return '${cont.substring(0, 10)}...';
      }
    
      String getCreatedAt() {
        try {
          // 曜日を表示したいときは「'yyyy/MM/dd(E) HH:mm:ss'」
          var fomatter = DateFormat('yyyy/MM/dd HH:mm:ss', 'ja_JP');
          return fomatter.format(createdAt);
        } catch (e) {
          print(e);
          return '';
        }
      }
    
      Memo({
        this.id,
        this.title,
        this.content,
        this.createdAt,
      });
    }
    
    memo_repository.dart
    import 'package:search_bar_sample_app/model/db/app_database.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    
    class MemoRepository {
      final AppDatabase _appDatabase;
    
      MemoRepository(this._appDatabase);
    
      Future<List<Memo>> loadAllMemo() => _appDatabase.loadAllMemo();
    
      Future<List<Memo>> search(String keyword) => _appDatabase.search(keyword);
    
      Future insert(Memo memo) => _appDatabase.insert(memo);
    
      Future update(Memo memo) => _appDatabase.update(memo);
    
      Future delete(Memo memo) => _appDatabase.delete(memo);
    }
    

    ui


    이어서'비망록 일람 화면''비망록 등록, 편집, 삭제 화면'을 만듭니다.
    main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_localizations/flutter_localizations.dart';
    import 'package:search_bar_sample_app/ui/memo_list/memo_list.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Search Bar App',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          routes: <String, WidgetBuilder>{
            '/': (_) => MemoList(),
          },
          localizationsDelegates: [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          supportedLocales: [
            const Locale('ja'),
          ],
        );
      }
    }
    

    비고 일람 화면


    memo_list.dart
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:search_bar_sample_app/ui/memo_detail/memo_detail.dart';
    import 'package:search_bar_sample_app/ui/memo_list/memo_list_view_model.dart';
    import 'package:search_bar_sample_app/model/db/app_database.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
    
    class MemoList extends StatelessWidget {
      
      Widget build(BuildContext context) {
        final vm = MemoListViewModel(MemoRepository(AppDatabase()));
        final page = _MemoListPage();
        return ChangeNotifierProvider(
          create: (_) => vm,
          child: Scaffold(
            // AppBarにTextFieldを配置することで、検索バーになる
            appBar: AppBar(
              title: TextField(
                style: const TextStyle(color: Colors.white),
                decoration: InputDecoration(
                  prefixIcon: Icon(Icons.search, color: Colors.white),
                  hintText: 'タイトルを検索',
                  hintStyle: const TextStyle(color: Colors.white),
                ),
                onChanged: (value) => vm.search(value),
              ),
            ),
            backgroundColor: Color(0xffF2F2F2),
            body: page,
            floatingActionButton: FloatingActionButton(
              child: Icon(Icons.add),
    	  // メモ登録画面に遷移する
              onPressed: () => page.goToMemoDetailScreen(context, null),
            ),
          ),
        );
      }
    }
    
    class _MemoListPage extends StatelessWidget {
      
      Widget build(BuildContext context) {
        final vm = Provider.of<MemoListViewModel>(context);
    
        if (vm.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }
    
        if (vm.memos.isEmpty) {
          return const Center(child: const Text('メモが登録されていません'));
        }
    
        return ListView.builder(
          itemCount: vm.memos.length,
          itemBuilder: (BuildContext context, int index) {
            var memo = vm.memos[index];
            return _buildMemoListTile(context, memo);
          },
        );
      }
    
      Widget _buildMemoListTile(BuildContext context, Memo memo) {
        return Card(
          child: ListTile(
            title: Text(
              memo.title,
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            subtitle: Text(memo.getContent()),
            trailing: Text(memo.getCreatedAt()),
    	// メモ編集・削除画面に遷移する
            onTap: () => goToMemoDetailScreen(context, memo),
          ),
        );
      }
    
      void goToMemoDetailScreen(BuildContext context, Memo memo) {
        var route = MaterialPageRoute(
          settings: RouteSettings(name: '/ui.memo_detail'),
          builder: (BuildContext context) => MemoDetail(memo),
        );
        Navigator.push(context, route);
      }
    }
    
    memo_list_view_model.dart
    import 'package:flutter/material.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
    
    class MemoListViewModel extends ChangeNotifier {
      final MemoRepository _repository;
    
      MemoListViewModel(this._repository) {
        loadAllMemo();
      }
    
      List<Memo> _memos = [];
    
      List<Memo> get memos => _memos;
    
      bool _isLoading = false;
    
      bool get isLoading => _isLoading;
    
      void loadAllMemo() async {
        _startLoading();
        _memos = await _repository.loadAllMemo();
        _finishLoading();
      }
    
      void search(String keyword) async {
        _startLoading();
        _memos = await _repository.search(keyword);
        _finishLoading();
      }
    
      void _startLoading() {
        _isLoading = true;
        notifyListeners();
      }
    
      void _finishLoading() {
        _isLoading = false;
        notifyListeners();
      }
    }
    

    설명 등록, 편집, 삭제 화면


    memo_detail.dart
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:search_bar_sample_app/model/db/app_database.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
    import 'package:search_bar_sample_app/ui/memo_detail/memo_detail_view_model.dart';
    
    class MemoDetail extends StatelessWidget {
      final Memo _memo;
    
      MemoDetail(this._memo);
    
      
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (_) => MemoDetailViewModel(_memo, MemoRepository(AppDatabase())),
          child: _MemoDetailPage(),
        );
      }
    }
    
    class _MemoDetailPage extends StatelessWidget {
      final GlobalKey<FormState> _globalKey = GlobalKey<FormState>();
    
      
      Widget build(BuildContext context) {
        final vm = Provider.of<MemoDetailViewModel>(context);
    
        return Scaffold(
          appBar: AppBar(
            title: Text(vm.isNew ? '登録' : '編集'),
            actions: [
              IconButton(
                icon: Icon(Icons.save),
                onPressed: () => _showSaveOrUpdateDialog(context),
              ),
              IconButton(
                icon: Icon(Icons.delete),
                onPressed: vm.isNew ? null : () => _showDeleteDialog(context),
              ),
            ],
          ),
          body: SafeArea(
            child: Form(
              key: _globalKey,
              child: ListView(
                padding: EdgeInsets.all(15),
                children: [
                  TextFormField(
                    decoration: const InputDecoration(labelText: 'タイトル'),
                    initialValue: vm.isNew ? '' : vm.memo.title,
                    validator: (value) => (value.isEmpty) ? 'タイトルを入力して下さい' : null,
                    onChanged: (value) => vm.setTitle(value),
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 20),
                    child: TextFormField(
                      decoration: InputDecoration(labelText: 'メモ'),
                      keyboardType: TextInputType.multiline,
                      maxLines: null,
                      initialValue: vm.isNew ? '' : vm.memo.content,
                      onChanged: (value) => vm.setContent(value),
                    ),
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 20),
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: Text('${vm.contentCounts} 文字'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    
      void _showSaveOrUpdateDialog(BuildContext context) {
        if (!_globalKey.currentState.validate()) return;
    
        var vm = Provider.of<MemoDetailViewModel>(context, listen: false);
    
        bool isNew = vm.isNew;
    
        String saveOrUpdateText = (isNew ? '保存' : '更新');
    
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              content: Text('メモを$saveOrUpdateTextしますか?'),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(context),
                  child: const Text('キャンセル'),
                ),
                TextButton(
                  onPressed: () =>
                      isNew ? _save(context, vm) : _update(context, vm),
                  child: Text(saveOrUpdateText),
                ),
              ],
            );
          },
        );
      }
    
      void _showDeleteDialog(BuildContext context) {
        var vm = Provider.of<MemoDetailViewModel>(context, listen: false);
    
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              content: const Text('メモを削除しますか?'),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(context),
                  child: const Text('キャンセル'),
                ),
                TextButton(
                  onPressed: () => _delete(context, vm),
                  child: const Text('削除'),
                ),
              ],
            );
          },
        );
      }
    
      void _save(BuildContext context, MemoDetailViewModel vm) async {
        _showIndicator(context);
        await vm.save();
        _goToMemoListScreen(context);
      }
    
      void _update(BuildContext context, MemoDetailViewModel vm) async {
        _showIndicator(context);
        await vm.update();
        _goToMemoListScreen(context);
      }
    
      void _delete(BuildContext context, MemoDetailViewModel vm) async {
        _showIndicator(context);
        await vm.delete();
        _goToMemoListScreen(context);
      }
    
      void _showIndicator(BuildContext context) {
        showGeneralDialog(
          context: context,
          barrierDismissible: false,
          transitionDuration: Duration(milliseconds: 300),
          barrierColor: Colors.black.withOpacity(0.5),
          pageBuilder: (
            BuildContext context,
            Animation animation,
            Animation secondaryAnimation,
          ) {
            return Center(child: CircularProgressIndicator());
          },
        );
      }
    
      void _goToMemoListScreen(BuildContext context) {
        Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
      }
    }
    
    memo_detail_view_model.dart
    import 'package:flutter/material.dart';
    import 'package:search_bar_sample_app/model/entity/memo.dart';
    import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
    import 'package:uuid/uuid.dart';
    
    class MemoDetailViewModel extends ChangeNotifier {
      final MemoRepository _repository;
    
      MemoDetailViewModel(memo, this._repository) {
        _memo = memo ?? initMemo();
        _isNew = (memo == null);
        _contentCounts = _memo.content.length;
        notifyListeners();
      }
    
      Memo _memo;
    
      Memo get memo => _memo;
    
      bool _isNew;
    
      bool get isNew => _isNew;
    
      int _contentCounts = 0;
    
      int get contentCounts => _contentCounts;
    
      Memo initMemo() {
        return Memo(
          id: Uuid().v4(),
          title: '',
          content: '',
          createdAt: null
        );
      }
    
      void setTitle(String title) {
        _memo.title = title;
        notifyListeners();
      }
    
      void setContent(String content) {
        _memo.content = content;
        _contentCounts = content.length;
        notifyListeners();
      }
    
      Future save() async {
        _memo.createdAt = DateTime.now();
        return await _repository.insert(_memo);
      }
    
      Future update() async {
        _memo.createdAt = DateTime.now();
        return await _repository.update(_memo);
      }
    
      Future delete() async => _repository.delete(_memo);
    }
    

    좋은 웹페이지 즐겨찾기