Better Programming

Advice for programmers.

Follow publication

Displaying SQLite Data From the Flutter App on the iOS Home Screen

Roman Cinis
Better Programming
Published in
11 min readAug 29, 2022
Background of this image is made by starline
The background of this image is made by the starline

Hey, devs! If you have already completed your Flutter project almost to perfection and are looking for other ways to improve UX when users work with your app (on iPhones, for example), there is one option — displaying your data from the app on the iOS home screen! If you have no experience with Swift development, it’s even better, because in this article, I will show you some basic differences between Dart vs. Swift and Flutter vs. SwiftUI, from a Flutter developer’s point of view.

Before we start

There are two small limitations — you can’t write home screen widgets on iOS in the Flutter framework, also those widgets are only available on iOS 14+. You will need to write them in Swift, but if you are already an experienced Flutter developer, coding with Swift will be a piece of cake for you. Dart is very similar to Swift in terms of functionality and purpose, there are only minor differences in syntax and notations.

You will see an example of a database services class that will have the same functionality in both Dart and Swift, and also basic differences between Flutter and SwiftUI. As you might notice, the Swift part we will add there will be the absolute minimum. It is a pretty good extension of your knowledge for relatively little effort, plus an additional bonus for your developer resume.

Source code with VCS commit history for you to see the changes is available here:

So grab it as a starting point/reference and let’s do it!

What we will do

For the demonstration, we will rewrite the classic Flutter “Counter” app to display the tap count on the home screen of your iPhone. So we will store the count data in one of the best local storage available in Flutter — SQLite. But you can easily use your preferred way, like key-value databases, NSUserDefaults, etc., which will be even easier. SQLite has the advantage of being platform independent so that your query will work wherever there is SQLite support.

By the way, Swift has “out-of-the-box” support for SQLite.

We will refresh the home screen data about every 15 minutes automatically but (mainly) directly from Flutter in real-time.

Getting Started

I assume you already have the project, if not — create a new one via the following command:

flutter create ...

Next, let’s create Swift’s Widget. Run from the root of your Flutter project command:

open ios/Runner.xcworkspace

Add a new Widget-Extension Target (File -> New Target -> Widget Extension) to your top-most Runner and give it your preferred name.

Add a checkmark on “Include Configuration Intent” for intent configuration; otherwise, it will create a static configuration. IDE will add a bunch of files, a few of them we will edit in further steps.

DatabaseService Swift File

Let’s add a DatabaseService.swift file in Xcode, we will need it in further steps. Right-click on the CounterWidget folder and select New File…

Select Swift File and provide a name, make sure to pick the proper target (not Runner one, but your Widget one).

App Group

We are not leaving Xcode yet, because:

iOS apps are “sandboxed”, so we have to create an App Group to connect the home screen widget with our Flutter app.

For this just add the capability App Group” to your Flutter App and your home screen widget. The group name (group.com.somename) must be the same for both Targets. That’s all for this moment, and it’s code time!

Code Part

Xcode’s bureaucracy is done, so let’s create a service that will provide the ability to store offline data on a device and it will be very similar in both languages. Since it’s an SQLite DB, the design of this service should therefore be able to do something like:

  • has to import SQLite lib,
  • has a proper name, like DatabaseService,
  • has private field db for SQLite itself,
  • has a method openDb() for DB opening and class initialization,
  • has a method getCount() for reading stored value,
  • has private immutable String fields for App Group ID, DB filename, and a SELECT statement which allows fetching the data from DB

On the Dart side, we will need also fields for the table name, id, and value identifiers, and a method for updating the counter and saving it to DB. So let’s create it step by step in each language and see some differences between them, let’s always start with Dart, which you are probably more familiar with.

Skeleton Classes

class DatabaseService {} // Dart class.

Could be the same in Swift, but here we should probably rather go with struct.

Because in Swift Classes are reference type, Structs are value type objects:

struct DatabaseService {}

Add it, and all further DB-related things in Swift to the DatabaseService.swift file.

Immutable Fields

In Dart language we are declaring private immutable fields this way:

static const _appGroupId = ‘group.com.example.flutterWidgetkit’;
static const _dbPath = ‘database.db’;
static const _queryStatementInt = “SELECT value FROM counter;”;

In Swift, we have to use a “private” keyword instead of an underscore in the name and a “let” keyword for immutability.

To specify type we have to write “: String” and text values are surrounded by double-quotes

private let queryStatementInt = “SELECT value FROM counter;”
private let appGroupId = “group.com.example.flutterWidgetkit”
private let dbPath: String = “database.db”

Imports

Dart/Flutter has no official support for SQLite, so pick your favorite package for that, in this example, I’ll use the sqflite package. First, add it to your project with the following command:

flutter pub add sqflite

Now import will look like this:

import ‘package:sqflite/sqflite.dart’;

And since Swift has built-in support for SQLite it will be on the Swift side:

import SQLite3

And also for some basic layers:

import Foundation

Database field

This will be again a private:

final Database _db;

And in Swift, it could be mutable and nullable because we should not just create a new database if Swift didn’t find a DB file (in opposite to Flutter, which should create a DB on the first run):

private var db: OpaquePointer?

As you can see this null-aware operator is the same as Dart one.

Class initialization

Since Dart doesn’t have async constructors/factories and database opening is always asynchronous operations we could go with a private constructor and static async method openDb() which will return the DatabaseService itself:

const DatabaseService._(this._db);static Future<DatabaseService> openDb() async {
...
return DatabaseService._(database);
}

In Swift return type is specified with arrow -> symbol and functions/methods are declared with the func keyword

init() {
db = openDb()
}
private func openDb() -> OpaquePointer? {
...
}

Get count data method

In Dart, it will be yet again async operation, because we are working with native platform features, and on both sides, nullable return since DB is not necessarily contained data (if no taps were made).

Future<int?> getCount() async {}

But in Swift we are only showing the data, so we can only return String:

func getCount() -> String? {}

Finalizing Dart Side

Ok so let’s fill our skeleton with real functionality in Flutter. So for opening and creating a DB we will need a few more compile-time constants: table name, id, and value identifiers.

To open the database we need to know the app group directory (since as you may remember, iOS apps are sandboxed), there are several packages for that, but I will use this one:

flutter pub add app_group_directory

And if we are into dependencies, let’s add a package for triggering refreshing of the iOS home screen widget manually:

flutter pub add flutter_widgetkit

By the way, there is great documentation in this package and also a link to an article with a similar example as this one, but using UserDefaults as local storage.

Now opening DB will be as easy as that:

static Future<DatabaseService> openDb() async {
final directory = await AppGroupDirectory.getAppGroupDirectory(_appGroupId);
if (directory == null) throw Exception(‘App Group $_appGroupId not found!’);final database = await openDatabase(join(directory.path, _dbPath),
version: 1,
onCreate: (db, _) => db.execute(
‘’’
CREATE TABLE $_table(
$_id TEXT PRIMARY KEY,
$_value INTEGER
)
‘’’,
),
);
return DatabaseService._(database);
}

To update count and force widget refresh let’s write (id never changes here but it’s just an example, right?):

Future<void> updateCount(int count) async {
/// Insert a new count to the database, if exists — just replace it.
await
_db.insert(
_table,
{_id: _id, _value: count},
conflictAlgorithm: ConflictAlgorithm.replace,
);
/// This will trigger a rebuild of Swift’s home-screen widget.
return
WidgetKit.reloadAllTimelines();
}

Finally to get value from DB add those few lines:

Future<int?> getCount() async {
final map = await _db.rawQuery(_queryStatementInt);
if (map.isEmpty) return null;
final maybeCount = map.first[_value];
return maybeCount is int ? maybeCount : null;
}

Now just pass your pass stored count and update the callback to your app and we are done. You can see the result on GitHub

Finalizing Swift DatabaseService

From the DatabaseService point of view, it’s quite easy to open we will use a similar approach as in Dart: open app group dir and look for database data (if exists):

private func openDb() -> OpaquePointer? {
let fileManager = FileManager.default
let directory = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
let dbFile = directory!.appendingPathComponent(dbPath)
var db: OpaquePointer?
if sqlite3_open(dbFile.path, &db) != SQLITE_OK {
print(“Error opening database”)
return nil
} else {
print(“Successfully opened connection to database at \(dbPath)”)
return db
}
}

As you may notice, nil is Swift’s equivalent of Dart's null value.

To get count, it won’t be that similar:

func getCount() -> String? {
var maybeCount: String?
var queryStatement: OpaquePointer?
let sqlState = sqlite3_prepare_v2(db, queryStatementInt, -1, &queryStatement, nil)
if sqlState == SQLITE_OK {
while sqlite3_step(queryStatement) == SQLITE_ROW {
let value = sqlite3_column_int(queryStatement, 0)
maybeCount = String(describing: value)
}
} else {
print(“SELECT statement could not be prepared”)
}
sqlite3_finalize(queryStatement)
return maybeCount
}

The result of Swift’s realization of DatabaseService is also available on GitHub:

Writing home screen widget for iOS

Now, we will need to write a home screen widget in SwiftUI, but great thing is, that we already have most of the code from Xcode.

Let’s take a look at what it provided us in CounterWidget.swift file. There is a bunch of classes but we can ignore most of them. One of the most important ones is:

struct SimpleEntry: TimelineEntry

TimelineEntry is a protocol that specifies when a widget should be displayed, here we can add the data we need to show on UI, in this example we will only add count with nullable String type (as you may remember, there is always a case when no there is no data yet stored in DB, but no worries we will handle it on UI):

struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let count: String?
}

You will see a lot of warnings, and yelling about missing “count”, you can just calm most of them by providing the default value “1” for now.

The next important thing is our UI(View), it’s described in

CounterWidgetEntryView : View

This contains the following code with access to entry.count value:

var entry: Provider.Entry

SwiftUI is sharing the same declarative nature as Flutter does, so let's replace the default:

Text(entry.date, style: .time),

with:

Text(entry.count ?? “0”),

Looks exactly like we do it in Flutter right?

But I’d rather add here some header above to describe what that number is, in Flutter, I’d go with a Column widget like that:

Column(
children: [
Text(“Count:”),
Text(entry.count ?? “0”),
]

In SwiftUI it will be very very similar:

VStack {
Text(“Count:”)
Text(entry.count ?? “0”).font(.title)
}

As you may see, it’s almost 1:1, with little differences in syntax and naming.

Swift’s VStack is Flutter’s Column like component.

And I’ve added the “title” style to the count to make it bigger. That’s all for UI.

The last but most important class to understand is IntentTimelineProvider. This protocol is responsible for building UI and showing/updating it on a timeline and it contains three methods:

  • placeholder: allows us to display a placeholder view to the user, and tells the WidgetKit what to render while the widget is loading.
  • getSnapshot: WidgetKit makes the snapshot request when displaying the widget in transient situations, such as when we are adding a widget to the screen.
  • getTimeline: most important one, it allows you to load data from DB and declare the next refresh moment of our widget. Our code uses .after update policy, which tells WidgetKit to ask for a new timeline once per about 15min, but we will rather rely on updates that are coming from the Dart side (via WidgetKit.reloadAllTimelines() call). Here we can initialize our DatabaseService, get value from it, and add it to the timeline.

You can see differences between default Flutter/SwiftUI generated files and the final result here:

And that’s it, now you should be able to display tap count from the Flutter app on your iOS home screen with Swift’s WidgetKit. Just run your app with the following command from the root of your project, and then run your home screen widget from Xcode (select proper target at the top menu):

flutter run

I hope that after reading this article you have gained enough courage to start programming in Swift on your own. It’s a Dart-like language that will be here with us for a while, regardless of Flutter’s existence, and as you can see it can be useful in your Flutter projects. Thanks for reading!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Roman Cinis
Roman Cinis

Written by Roman Cinis

I'm a multiple-time Google-awarded Flutter/Dart developer, with a great love for interactive animations and good UX. Big Rive fan.

No responses yet

Write a response