Powrót

SQLite i flutter - z czym to się je.

W tym poradniku przedstawiam, jak napisać prostą aplikację do dodawania i usuwania studentów za pomocą interfejsu użytkownika w bazie danych.


💡 Zanim przeczytasz upewnij się, że:

  • 🔗 Posiadasz zainstalowany flutter,
  • 🔗 Posiadasz skonfigurowane IDE
  • 🔗 Zapoznałeś się ze składnią języka Dart
  • 🔗 Wykonałeś pierwsze kroki we flutterze

☕ Jeżeli wszystko ogarnięte to kawusia w dłoń i lecimy.

📚Czym jest flutter i SQLite

Flutter to framework służący do tworzenia aplikacji wieloplatformowych. Pozwala nam na napisanie kodu, który działa równocześnie na platformach Android, iOS, Linux, Windows i MacOS. Flutter to projekt opensource stworzony przez Googla, więc możemy być pewni, że framework będzie wspierany jeszcze przez wiele lat.

SQLite to biblioteka C implementująca silnik bazy danych SQL. Została ona bardzo dobrze przetestowana co świadczy o jej niezawodności. Całość mieści się w pojedynczym pliku systemowym, a jego format jest wieloplatformowy. SQLite wypada bardzo dobrze w testach wydajności przy obsłudze jednego użytkownika. To wszystko sprawia, że SQLite sprawdza się świetnie w świecie mobile.


⚙️ Krok 1. Dependencies

Stwórzmy nowy projekt, do którego dodamy dwie nowe zależności.

  • sqflite to pakiet udostępniający nam klasy oraz funkcje do obsługi bazy danych SQLite
  • path_provider to pakiet udostępniający funkcje do lokalizowania bazy danych na dysku

Do pliku pubspec.yaml dopisz najnowsze wersje tych pakietów. Nie zapomnij zaktualizować zależności.

dependencies:  
  flutter:  
    sdk: flutter  
  sqflite: ^1.3.0  
  path_provider: ^1.6.22

🌳 Krok 2. Przygotowanie struktury projektu

Usuńmy folder test, nie będzie on nam potrzebny.

W folderze lib stwórzmy folder database w którym będziemy trzymać całą logikę bazy danych naszej aplikacji. Stwórzmy w tym folderze plik database.dart w którym obsłużymy naszą bazę.

W folderze database stwórzmy folder models (będziemy tu przechowywać klasy, które reprezentują model danych w bazie). Dodajmy do niego nowy plik StudentModel.dart.

+ --- + lib/
      |   main.dart
      |
      + --- + database/
            |   database.dart
            |
            + --- models/
                    StudentModel.dart

Gdy mamy przygotowaną strukturę, pora zabrać się za kodzenie 🧑‍💻.


📐 Krok 3. Model Class

Aby zapewnić spójną komunikację między bazą danych a naszą aplikacją musimy zadbać o odpowiednie przechowywanie spójnego modelu danych. Posłuży nam do tego klasa Student. Student będzie posiadał 4 pola. Typy danych będą różne dla języka Dart i SQL. Pole id będzie kluczem głównym.

Pole klasy Dart SQLite
🗝️ id int INT
firstName String TEXT
lastName String TEXT
grade int INT

🔗 Typy danych SQLite

Implementacja wygląda następująco. Pola posiadają typ final, ponieważ chcemy aby pierwsza przypisana do nich wartość była stała. Konstruktor domyślny z listą inicjalizacyjną.

class Student {  
  final int id;  
  final String firstName;  
  final String lastName;  
  final int grade;  
  
  Student({  
    this.id,  
    this.firstName,  
    this.lastName,  
    this.grade  
  });  
}

To nie koniec. SQLite z naszą aplikacją wymienia się danymi w postaci Mapy. Aby sprawnie przechodzić z instancji klasy na mapę i odwrotnie należy zaimplementować odpowiednie do tego metody. Zmapujemy ciąg znaków na dynamiczny typ danych, ponieważ posiadamy różne rodzaje danych w modelu Map<String, dynamic>.

class Student {  

// ...

  factory Student.fromMap(  
    Map<String, dynamic> map) => new Student(  
      id: map["id"],  
      firstName: map["first_name"],  
      lastName: map["last_name"],  
      grade: map["grade"]  
  );  
  
  Map<String, dynamic> toMap() => {  
    "id": id,  
    "first_name": firstName,  
    "last_name": lastName,  
    "grade": grade  
  };
}

Zauważ, że konstruktor klasy Student fromMap posiada słowo kluczowe factory (tak zwany factory constructor) dzięki któremu możemy obsłużyć logikę tworzenia instancji, której nie jest w stanie obsłużyć lista inicjalizacyjna.

🔗 Więcej o factory consturctor na dart.dev oraz stackoverflow.


📊 Krok 4. DatabaseProvider

Pora zadbać o inicjalizację naszej bazy danych. Skorzystamy z wzorca Singleton dzięki któremu obiekt DatabaseProvider będzie jedynym tego rodzaju obiektem w naszej aplikacji. Taką logikę uzyskujemy za pomocą pola static instancji klasy oraz prywatnego konstruktora. Dzięki temu instancja istnieje cały czas, a prywatny konstruktor uniemożliwia stworzenia kolejnego obiektu z zewnątrz.

  
class DatabaseProvider {  
  // private constructor  
  DatabaseProvider.internal();  
  
  // static instance  
  static final DatabaseProvider db = DatabaseProvider.internal();  
  
  // SQLite database  
  Database _database;  
}

Teraz potrzebujemy funkcji, która będzie zwracała nam połączenie z bazą danych lub tworzyła je, jeżeli jeszcze nie zostało ustanowione.

class DatabaseProvider { 
 
// ...

  Future<Database> get database async {  
    if(_database != null) return databaseInstance();  
    _database = await databaseInstance();  
    return _database;
  }  
  
  Future<Database> databaseInstance() async {  
    Directory dir = await getApplicationDocumentsDirectory();  
    String path = join(dir.path, "app_database.db");  
    return await openDatabase(  
      path,  
      version: 1,  
      onCreate: (db, v) async {  
        await db.execute("CREATE TABLE IF NOT EXISTS `students` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `first_name` TEXT, `last_name` TEXT, `grade` INT)");  
      }  
    );  
  }  
}

Zauważ, że powyższy kod nie zadziała nam jeżeli nie dodamy odpowiednich pakietów.

import 'package:sqflite/sqflite.dart';              // Database, openDatabase()
import 'package:path/path.dart';  	            // join()
import 'package:path_provider/path_provider.dart';  // getApplicationDocumentsDirectory()
import 'dart:io'; 				    // Diretory

🚀 Krok 5. CRUD

Stworzymy teraz funkcje do tworzenia, pobierania, aktualizowania i usuwania studentów. Należy też dodać model Student do naszego pliku database.dart .

  1. Pobieranie studentów lub studenta po id
// ...
// ...

import 'models/StudentModel.dart';

class DatabaseProvider { 

// ...

  Future<List<Student>> getAllStudents() async {  
    final db = await database;  
    var response = await db.query('students');  
    List<Student> list = response.map(  
      (s) => Student.fromMap(s)  
    ).toList();  
    return list;  
  }  
  
  Future<Student> getStudentById(int id) async {  
    final db = await database;  
    var response = await db.query(  
      'students',  
      where: "id = ?",  
      whereArgs: [id]  
    );  
    return response.isEmpty ? Student.fromMap(response.first) : null;  
  }

// ...
  1. Tworzenie studenta
// ...

  Future<int> addStudent(Student student) async {  
    final db = await database;  
    int id = await db.insert(  
      'students',  
      student.toMap(),  
      conflictAlgorithm: ConflictAlgorithm.replace  
    );  
    return id;

// ...
  1. Usuwanie studentów lub studenta po id
// ...

  deleteAllStudents() async {  
    final db = await database;  
    db.delete("students");  
  }  
  
  deleteStudent(int id) async {  
    final db = await database;  
    db.delete("students", where: "id = ?", whereArgs: [id]);  
  }

// ...
  1. Aktualizowanie studenta po id
// ...

  Future<int> updateStudent(Student student) async {  
    final db = await database;  
    var id = await db.update(  
      "students",   
      student.toMap(),  
      where: "id = ?",   
      whereArgs: [student.id]  
    );  
    return id;  
  }
}

🌟 Krok 6. UI

Nasze bazodanowe API w postaci DatabaseProvider jest już gotowe. Pora wykorzystać je w praktyce! Przejdźmy do pliku main.dart. Stwórzmy Stateful Widget, który będzie przechowywał listę naszych studentów, zmienną isLoading informującą czy dane są pobierane oraz metodę fetchStudents, która będzie pobierała naszych studentów.

void main () => runApp(MaterialApp(home: HomePage()));  
  
class HomePage extends StatefulWidget {  
    
  _HomePageState createState() => _HomePageState();  
}  
  
class _HomePageState extends State<HomePage> {  
  bool isLoading;  
  List<StudentDriver> studentsList;  
  
    
  void initState() {  
    super.initState();  
    isLoading = true;  
    fetchStudents();  
  }  
  
 //...
  
  void fetchStudents() async {  
    setState(() => isLoading = true);  
    final tmpList = await DatabaseProvider.db.getAllStudentDrivers();  
    setState(() {  
      isLoading = false;  
      studentsList = tmpList;  
    });  
  }  
}

Struktura Widgetów naszej aplikacji będzie wyglądała następująco.

  
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: Center(child: Text('SQLite Demo')),
      ),
      body: Column(
        children: <Widget>[
          form(),
          list(),
        ],
      ),
    );
  }

Wykorzystamy prostą funkcję split do dzielenia ciągu znaków na dwa pola - imię i nazwisko. Ocena będzie wartością losowaną - od 1 do 5. Aby korzystać z wartości losowych, musimy dodać w nagłówku naszego pliku linijkę

import 'dart:math';

Implementacja formularza.

  final textController = TextEditingController();  
  final formKey = GlobalKey<FormState>();

// ...

  form() {
    return Form(
      key: formKey,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(15.0),
            child: TextFormField(
              decoration: InputDecoration(
                labelText: 'Enter student full name'
              ),
              controller: textController,
              validator: (value) =>
              value.isEmpty ? "Field is empty" : null
            ),
          ),
          ElevatedButton(
            onPressed: () async {
              final words = textController.text.split(' ');
              if(formKey.currentState.validate()) {
                await DatabaseProvider.db.addStudent(
                  new Student(
                    firstName: words[0],
                    lastName: words[1],
                    grade: (Random().nextInt(4) + 1)
                  )
                );
                fetchStudents();
                textController.clear();
              }
            },
            child: Text("Add Student")
          )
        ]
      )
    );
  }

Implementacja listy studentów.

  list() {
    return Expanded(
      child: isLoading
        ? Center(child: CircularProgressIndicator())
        : ListView.builder(
          scrollDirection: Axis.vertical,
          itemCount: students.length,
          itemBuilder: (context, index) {
            final student = students[index];
            return Dismissible(
              background: Container(color: Colors.red),
              key: Key(student.id.toString()),
              onDismissed: (direction) async {
                await DatabaseProvider.db.deleteStudent(student.id);
                fetchStudents();
              },
              child: ListTile(
                title: Text("${student.firstName} ${student.lastName}"),
                subtitle: Text('id: ${student.id} grade: ${student.grade}'),
              ),
            );
          }
        )
    );
  }

👏 Efekt końcowy


💬 Podsumowanie

Zapoznałeś się z obsługą sqlfite. Teraz jesteś w stanie budować aplikacje zapamiętujące dane. To otwiera przed Tobą pełnie możliwości. Co dalej? Zachęcam do rozbudowania powyższej aplikacji (walidacja danych, kolejne pole formularza, aktualizowanie studenta) oraz zapoznania się z floor. Dziękuję za przeczytanie tego artykułu i życzę Ci powodzenia w dalszym rozwijaniu się.

- Tobiasz Ciesielski tobiaszciesielski

Jeśli masz jakieś uwagi lub sugestie podeślij nam je na adres kontakt@akai.org.pl lub kontrybuuj do naszego repozytorium.