Перейти к содержимому

Как подключить базу данных к проекту flutter

  • автор:

#10 – Подключение базы данных Firebase

#10 – Подключение базы данных Firebase

К любому Flutter проекту можно быстро подключить базу данных. За урок мы научимся подключать и работать с базой данных Firebase. Нами будет переделана программа «Список дел» и все данные из программы будут хранится в удаленной базе Firebase.

Видеоурок

Полезные ссылки:

  • Официальный сайт Firebase ;
  • Инструкции относительно Cloud Firestore .

Исправление ошибок

При запуске проекта может выдаваться ошибка связанная с тем, что FireBase не запущен. Такое может происходить по причине того, что его инициализацию нужно прописывать в основном методе.

Для устранения ошибки в файле «main.dart» пропишите:

// Подключени всех сцен import 'package:flutter_todo/pages/home.dart'; import 'package:flutter_todo/pages/main_screen.dart'; import 'package:firebase_core/firebase_core.dart'; void main() async < // Инициализация в основной функции WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MaterialApp( theme: ThemeData( primaryColor: Colors.deepOrangeAccent, ), initialRoute: '/', routes: < // Маршрутизация '/': (context) =>MainScreen(), '/todo': (context) => Home(), >, )); >

База данных Firebase

При разработке Flutter программ вы можете использовать практически любую базу данных. В вашем распоряжение такие БД, как:

  • SQLite;
  • MySQL;
  • MongoDB ;
  • Firebase ;
  • и множество других.

В ходе урока мы использовали БД Firebase, так как она одна из наиболее простых для интеграции в Flutter и обладает широким спектром возможностей.

Благодаря тому, что Firebase была куплена компанией Google в 2014 году, то интеграция этой БД и Flutter стала легким и быстрым процессом. Напомним, что Flutter также принадлежит компании Google.

Как подключить базу данных к проекту flutter

Бюджет: ожидает предложений

Дедлайн: 27.04.2023

Требуется реализовать подключение базы данных к приложению написанном на языке программирования flutter в мобильном приложении. Далее, в процессе работы программы после нажатия кнопки choose a gift, пользователя переносит на страницу с «предложенным подарком», фото подарка должно подгружаться из базы данных и отображаться в овале на фото 2, а так же, должен выводиться под фото адрес магазина с этим подарком так же из базы данных. База данных будет заполняться вручную мной. Пример строения таблицы picture («адрес расположения фотографии номер 1», «адрес расположения фотографии номер 2»), address (‘ул. Марковского 1’, ‘ул. Марковского 2’). «Подарок» должен выбираться рандомно из базы данных. По базам данных предпочтительно использовать Firebase. Срок выполнения до 27 апреля 04:00 по МСК.

Опубликован:

26.04.2023 | 11:27 [поднят: 26.04.2023 | 11:27] [последние изменения: 26.04.2023 | 11:22]

Подключение SDK к проекту

Для использования SDK на платформе Flutter необходимо внедрить в Ваш Flutter проект нативные SDK для iOS и Android. С этим Вам поможет данная инструкция.

iOS 

Перейдите в Вашем проекте в папку ios , откройте Runner.xcodeproj с помощью Xcode , перетащите файл переданный вам с договором в Frameworks, Libraries, and Embedded Content , а также выставите Embed & Sign .

В файле info.plist проекта Runner.xcodeproj должны быть добавлены следующие параметры:

 key>DTXAutoStartkey> string>falsestring> key>LSApplicationQueriesSchemeskey> array> string>sbolidexternalloginstring> string>sberbankidexternalloginstring> array> key>NSAppTransportSecuritykey> dict> key>NSExceptionDomainskey> dict> key>gate1.spaymentsplus.rukey> dict> key>NSExceptionAllowsInsecureHTTPLoadskey> true/> dict> key>ift.gate2.spaymentsplus.rukey> dict> key>NSExceptionAllowsInsecureHTTPLoadskey> true/> dict> key>cms-res.online.sberbank.rukey> dict> key>NSExceptionAllowsInsecureHTTPLoadskey> true/> dict> dict> dict> key>NSBluetoothAlwaysUsageDescriptionkey> string>Данные Bluetooth собираются и отправляются на сервер для безопасного проведения оплатыstring> key>NSBluetoothPeripheralUsageDescriptionkey> string>Данные Bluetooth собираются и отправляются на сервер для безопасного проведения оплатыstring> 

Для успешной авторизации в среде банка необходимо в настройках таргета проекта Runner.xcodeproj зарегистрировать deeplink вашего приложения.

Добавьте Access wi-fi information в Capabilities таргета вашего проекта Runner.xcodeproj . Для этого выберите ваш таргет → Signing & Capabilities → +Capability → Access wi-fi information.

Android 

Перейдите в Вашем проекте в папку android , откройте проект с помощью Android Studio . Для получения зависимости из maven репозитория необходимо добавить его в файл settings.gradle.

 dependencyResolutionManagement   ...  repositories   google() mavenCentral() ...  ⁣ maven   ⁣name = "GitHubPackages"  url = uri("*URL из договора*")  credentials   ⁣ username = "*username из договора*"  ⁣ password = "*password из договора*" > > > > 

Далее нужно перейти в build.gradle Вашего модуля и добавить зависимости внутрь блока dependencies .

 dependencies   ...  implementation '*название зависимости из договора*:x.y.z' ... > 

Flutter. Локальная база данных

Ранее мы писали статью про реализацию паттерна MVVM на Флаттере. В комментариях к ней просили разобрать связку нашего приложения с базой данных.

Стоит заметить, что локальная БД в данном случае будет использоваться для кеширования данных, получаемых с бэкенда, однако взаимодействие с бэком не будет рассматриваться здесь, так как это тема, достойная отдельной статьи.

Меня зовут Ричард, и я младший разработчик в компании Digital Design.

Здесь будет разобрана конкретная реализация, однако я постараюсь использовать максимально общий язык для описания происходящего.

Проблема

Предположим, что у нас есть бэк с основной базой данных и несколько мобильных клиентов нашего ToDoList’а (опять же отсылаю к прошлой статье). На мобилке у нас реализована логика взаимодействия с нашим бэком, однако возникает проблема: где хранить полученные данные?

Мы можем создать массив где-то в нашей программе, но при закрытии приложения мы будем терять все данные. К тому же в таком случае нам придётся держать в памяти сразу все полученные данные, что может быть проблематично.

Что же делать в таком случае? Использовать локальную базу данных!

Подготовка

Использовать мы будем SQLite, так как она достаточно быстрая и легковесная для работы на мобильном устройстве.

Для реализации задумки нам нужно добавить следующие пакеты в проект:

  • sqflite — для подключения SQLite базы данных в Flutter
  • path — для облегчения работы с путями (необязательно)

Для этого можно отредактировать файл pubspec.yaml, добавив в него зависимость от этих пакетов либо выполнив команду в консоли:

flutter pub add sqflite path — данная команда, собственно, и добавит зависимости в тот же файл и скачает пакеты последней версии.

Реализация

Структура базы данных

Создадим файл, в котором мы опишем структуру нашей будущей БД, и добавим его как asset в pubspec.yaml

Внутри файла создадим таблицу с нашими ToDo’шками.

CREATE TABLE t_ToDoItem ( [id] INTEGER NOT NULL PRIMARY KEY ,[name] TEXT NOT NULL ,[isDone] INTEGER );

Забегая чуть вперёд, скажу, что при добавлении нескольких таблиц лучше всего использовать одинаковое название для первичных ключей и один паттерн именования таблиц.

Подготовка моделей для работы с базой данных

Определим абстрактный класс для работы с БД. В нём мы объявим:

  • Id, не привязанный к конкретному типу;
  • методы по перегонке нашего объекта в Map (и обратно), так как вся работа с базой происходит через неё.
abstract class DbModel < final T id; DbModel(); // Используем при получении данных из базы static fromMap(Map map) <> // Используем при отправке данных в базу Map toMap() => Map.fromIterable([]); >

Теперь реализуем интерфейс DbModel в нашей модельке ToDoItem.

class ToDoItem implements DbModel < @override final int id; // переопределяем поле и указываем определённый тип final String name; final bool isDone; ToDoItem(< required this.id, required this.name, this.isDone = false, >); factory ToDoItem.fromMap(Map map) => _$ToDoItemFromMap(map); @override Map toMap() < return < 'id': id, 'name': name, 'isDone': isDone ? 1 : 0, // В sqlite нет типа данных bool, поэтому мы храним наш флаг в виде числа >; > static ToDoItem _$ToDoItemFromMap(Map map) => ToDoItem( id: map['id'], name: map['name'], isDone: map['isDone'] == 1, // При получении из базы обратно переводим в bool );

Создание БД

База данных у нас будет синглтоном, который запускается вместе с приложением. Sqflite предоставляет нам класс Database, в котором содержится интерфейс обращения к БД.

Создадим класс DB, в котором будет описана наша логика работы с базой: её инициализация, создание и CRUD-операции.

Начнём с инициализации. Для этого мы должны указать путь до файла с БД и открыть его.

class DB < DB._(); // Приватный конструктор static final DB instance = DB._(); // экземпляр с которым будем работать static late Database _db; // “интерфейс” для работы с sqflite static bool _isInitialized = false; Future init() async < if (!_isInitialized) < var databasePath = await getDatabasesPath(); // получение дефолтной папки для сохранения файла БД var path = join(databasePath, "db_v1.0.2.db"); _db = await openDatabase(path, version: 1, onCreate: _createDB); _isInitialized = true; >> > 

При открытии БД sqflite проверяет существование указанного файла. Если его нет, то отрабатывается метод _createDB, который мы ему передаём.

Future _createDB(Database db, int version) async < // db_init.sql должен быть прописан в pubspec.yaml var dbInitScript = await rootBundle.loadString('assets/db_init.sql'); dbInitScript.split(';').forEach((element) async < if (element.isNotEmpty) < await db.execute(element); >>); >

В нашем случае мы просто считываем строку из файла, разбиваем по символу ‘;’ и говорим базе выполнить каждую из полученных строк.

Фабрики и таблицы

Так как из базы мы получаем данные в виде Map, то имеет смысл хранить все фабрики в одном месте и по передаваемому типу получать нужную фабрику.

 static final _factories = map)> < ToDoItem: (map) =>ToDoItem.fromMap(map), >;

Также при работе с БД нам постоянно потребуется указывать название рабочей таблицы, поэтому напишем метод, который для указанного класса вернёт нужное название:

String _dbName(Type type) < if (type == DbModel) < throw Exception("Type is required"); >return "t_$"; >

CRUD-операции

Future insert(T model) async => await _db.insert( _dbName(T), // Получаем имя рабочей таблицы model.toMap(), // Переводим наш объект в мапу для вставки conflictAlgorithm: null, // Что должно происходить при конфликте вставки nullColumnHack: null, // Что делать, если not null столбец приходит как null );
Future get(dynamic id) async < var res = await _db.query( _dbName(T), where: 'id = ? ', // Прописываем в виде строки нужное нам условие и на месте сравниваемого значения ставим ‘?’ whereArgs: [id], // значения, передаваемые в этом массиве будут подставляться вместо ‘?’ в запросах. Порядок аргументов ВАЖЕН! ); return res.isNotEmpty ? _factories[T]!(res.first) : null; >

Для получения всех строк из таблицы достаточно убрать условие where. В таком случае есть смысл использовать параметры offset и take для ограничения количества возвращаемых записей.

  • Read с ограничением по количеству записей:
Future> getAll(< int? take, int? skip, >) async < Iterable> query = query = await _db.query( _dbName(T), offset: skip, // сколько строк нужно пропустить из конечной выборки limit: take, // количество возвращаемых строк ); var resList = query.map((e) => _factories[T]!(e)).cast(); return resList; >
Future update(T model) async => _db.update( _dbName(T), model.toMap(), where: 'id = ?', // без этого все строки таблицы будут обновлены whereArgs: [model.id], );
Future delete(T model) async => _db.delete( _dbName(T), where: 'id = ?', // если не указывать, то удалятся все строки whereArgs: [model.id], );

Довольно удобно бывает объединить операции create и update в одном методе.

Future createUpdate(T model) async < var dbItem = await get(model.id); var res = dbItem == null ? insert(model) : update(model); return await res; >

Data-сервис

Проделанная до этого нами работа — это необходимый минимум. Однако в процессе разработки обнаруживается, что мы часто используем одни и те же запросы. Так создадим специальный сервис для хранения таких запросов.

Получение списка задач:

Future> getToDoList(< int take = 10, int skip = 0, >) async < var items = await DB.instance.getAll(skip: skip, take: take); return items.toList(); >

Все запросы, которые были написаны для нашего ToDoList’а, мы разбирать не будем, так как они достаточно тривиальны (просто вызывают соответствующие методы из нашего класса DB). При желании можете посмотреть их здесь.

Применение

Вернёмся к тому, что было сделано в прошлой статье. У нас были следующие классы:

class _ModelState < final Listitems; // Пока этот флаг true, на экране видна загрузка final bool isLoading; _ModelState(< this.items = const [], this.isLoading = false, >); _ModelState copyWith(< List? items, bool? isLoading, >) < return _ModelState( items: items ?? this.items, isLoading: isLoading ?? this.isLoading, ); >>
class _ViewModel extends ChangeNotifier < var _state = _ModelState(); _ModelState get state =>_state; set state(_ModelState val) < _state = _state.copyWith(items: val.items, isLoading: val.isLoading); notifyListeners(); >addItem(CreateTodoItemModel model) < // логика добавления задачи >deleteItem(ToDoItem item) < // логика удаления >toggleItem(ToDoItem item) < // переключение состояния "выполнения" задачи >>

Добавим сюда наш DataService и создадим метод, который при создании экземпляра _ViewModel будет загружать задачи из базы.

class _ViewModel extends ChangeNotifier < final _api = RepositoryModule.apiRepository(); final _syncService = SyncService(); final _dataService = DataService(); var _state = _ModelState(); _ModelState get state =>_state; set state(_ModelState val) < _state = _state.copyWith(items: val.items, isLoading: val.isLoading); notifyListeners(); >_ViewModel() < _asyncInit(); >_asyncInit() async < if (!state.isLoading) < state = state.copyWith(isLoading: true); await _syncService.syncTodoList(); var todoItems = await _dataService.getToDoList(); state = state.copyWith(items: todoItems, isLoading: false); >> addItem(CreateTodoItemModel model) async < // отправляем запрос о добавлении на бэк await _api.addTodoItem(model); await _asyncInit(); // подтягиваем актуальные данные >toggleItem(ToDoItem item) async < // отправляем запрос об изменении на бэк await _api.updateItemStatus(item.id); await _asyncInit(); >deleteItem(ToDoItem item) async < await _api.deleteTodoItem(item.id); // запрос об удалении записи await _dataService.deleteTodoItem(item.id); // удаляем запись из локальной БД await _asyncInit(); >>

Что же находится внутри SyncService? Просто получение данных с API и добавление их в базу:

class SyncService < final _api = ApiModule.api(); final _dataService = DataService(); Future syncTodoList() async < var todoList = await _api.getTodoList(skip, take); await _dataService.rangeUpdateEntities(todoList); >>

Когда мы всё добавили — настала пора проверять работу нашего приложения.

Вуаля. Теперь все наши задачи не теряются при закрытии приложения.

Итог

Итак, мы рассмотрели вариант использования локальной базы данных в нашем мобильном приложении. Может быть, вы знаете более удобный способ организации работы с мобильной БД. Поделитесь личным опытом в комментариях!

Что делать дальше? Научить своё приложение обмениваться данными с бэкендом! В следующей статье я подробно расскажу о том, как организовать взаимодействие между мобильным приложением и API.

  • разработка
  • мобильная разработка
  • flutter
  • dart
  • база данных во flutter
  • кеширование
  • мобильность
  • Блог компании Digital Design
  • Разработка мобильных приложений
  • Dart
  • Flutter

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *