This commit is contained in:
Andika Tanuwijaya
2025-07-02 00:39:36 +07:00
parent e37388a6c9
commit 578c7ae53a
20 changed files with 575 additions and 103 deletions

View File

@@ -1,28 +1,4 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -2,12 +2,16 @@ PODS:
- Flutter (1.0.0)
- integration_test (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- turso_dart (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- turso_dart (from `.symlinks/plugins/turso_dart/ios`)
EXTERNAL SOURCES:
@@ -15,12 +19,15 @@ EXTERNAL SOURCES:
:path: Flutter
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
turso_dart:
:path: ".symlinks/plugins/turso_dart/ios"
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
turso_dart: 60d33b3052e957b90999c5be8cf2ec1201db1731
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5

View File

@@ -0,0 +1,9 @@
import 'package:turso_dart/turso_dart.dart';
Future<void> bootstrapDatabase(LibsqlClient client, {bool sync = false}) async {
await client.connect();
await client.execute("drop table if exists tasks");
await client.execute(
"create table if not exists tasks (id integer primary key, title text, description text, completed integer)",
);
}

View File

@@ -0,0 +1,2 @@
export 'task_list_cubit.dart';
export 'task_list_state.dart';

View File

@@ -0,0 +1,64 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:turso_dart_example/features/task/blocs/task_list_state.dart';
import 'package:turso_dart_example/features/task/models/models.dart';
import 'package:turso_dart_example/features/task/repositories/repositories.dart';
class TaskListCubit extends Cubit<TaskListState> {
final TaskRepository _taskRepository;
StreamSubscription? _subscription;
TaskListCubit(this._taskRepository) : super(TaskListInitial()) {
getTasks();
_taskRepository.replicaChanges().then((stream) {
_subscription = stream?.listen((event) {
getTasks();
});
});
}
Future<void> getTasks() async {
emit(TaskListLoading());
try {
final tasks = await _taskRepository.getTasks();
emit(TaskListLoaded(tasks));
} catch (e) {
emit(TaskListError(e.toString()));
}
}
Future<void> addTask(Task task) async {
try {
await _taskRepository.addTask(task);
await getTasks();
} catch (e) {
emit(TaskListError(e.toString()));
}
}
Future<void> deleteTask(int id) async {
try {
await _taskRepository.deleteTask(id);
await getTasks();
} catch (e) {
emit(TaskListError(e.toString()));
}
}
Future<void> markTasksAsCompleted(List<int> ids) async {
try {
await _taskRepository.markTasksAsCompleted(ids);
await getTasks();
} catch (e) {
emit(TaskListError(e.toString()));
}
}
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:turso_dart_example/features/task/models/models.dart';
abstract class TaskListState {}
class TaskListInitial extends TaskListState {}
class TaskListLoading extends TaskListState {}
class TaskListLoaded extends TaskListState {
final List<Task> tasks;
TaskListLoaded(this.tasks);
}
class TaskListError extends TaskListState {
final String message;
TaskListError(this.message);
}

View File

@@ -0,0 +1 @@
export 'task.dart';

View File

@@ -0,0 +1,13 @@
class Task {
final int id;
final String title;
final String description;
final bool completed;
const Task({
required this.id,
required this.title,
required this.description,
required this.completed,
});
}

View File

@@ -0,0 +1 @@
export 'task_repository.dart';

View File

@@ -0,0 +1,15 @@
import 'dart:async';
import 'package:turso_dart_example/features/task/models/models.dart';
abstract class TaskRepository {
Future<Stream?> replicaChanges();
Future<List<Task>> getTasks();
Future<void> addTask(Task task);
Future<void> deleteTask(int id);
Future<void> markTasksAsCompleted(List<int> ids);
}

View File

@@ -0,0 +1,4 @@
export 'blocs/blocs.dart';
export 'models/models.dart';
export 'repositories/repositories.dart';
export 'task_list.dart';

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
class TaskAdd extends StatefulWidget {
const TaskAdd({super.key});
@override
State<TaskAdd> createState() => _TaskAddState();
}
class _TaskAddState extends State<TaskAdd> {
final formKey = GlobalKey<FormState>();
final titleController = TextEditingController();
final descriptionController = TextEditingController();
@override
Widget build(BuildContext context) {
return BottomSheet(
onClosing: () {},
enableDrag: false,
builder: (context) {
return SingleChildScrollView(
child: Form(
key: formKey,
autovalidateMode: AutovalidateMode.disabled,
child: Padding(
padding: const EdgeInsets.all(24).add(EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Add new task', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
const Text('Name'),
TextFormField(
controller: titleController,
textCapitalization: TextCapitalization.sentences,
onTap: () {
titleController.selection = TextSelection(
baseOffset: 0,
extentOffset: titleController.text.length,
);
},
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Must not be empty';
}
return null;
},
),
const SizedBox(height: 16),
const Text('Description'),
TextFormField(
controller: descriptionController,
textCapitalization: TextCapitalization.sentences,
onTap: () {
titleController.selection = TextSelection(
baseOffset: 0,
extentOffset: titleController.text.length,
);
},
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Must not be empty';
}
return null;
},
),
const SizedBox(height: 32),
FilledButton(
onPressed: () {
if (!formKey.currentState!.validate()) return;
Navigator.maybePop(context, {
'title': titleController.text,
'description': descriptionController.text,
});
},
child: const Text('Add task'),
),
],
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:turso_dart_example/features/task/blocs/blocs.dart';
import 'package:turso_dart_example/features/task/models/task.dart';
import 'package:turso_dart_example/features/task/repositories/repositories.dart';
import 'package:turso_dart_example/features/task/task_add.dart';
class TaskList extends StatefulWidget {
const TaskList({super.key, this.syncOnNetworkChange = false});
final bool syncOnNetworkChange;
@override
State<TaskList> createState() => _TaskListState();
}
class _TaskListState extends State<TaskList> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TaskListCubit(context.read<TaskRepository>()),
child: SafeArea(
child: Scaffold(
appBar: AppBar(title: const Text('Tasks')),
floatingActionButton: Builder(
builder: (context) {
return FloatingActionButton(
onPressed: () async {
final taskData =
await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
builder: (_) => const TaskAdd(),
);
if (taskData == null || !context.mounted) return;
await context.read<TaskListCubit>().addTask(
Task(
id: -1,
title: taskData["title"],
description: taskData["description"],
completed: false,
),
);
},
child: const Icon(Icons.add),
);
},
),
body: Padding(
padding: const EdgeInsets.all(24),
child: BlocBuilder<TaskListCubit, TaskListState>(
builder: (context, state) {
return switch (state) {
TaskListInitial() => const SizedBox.shrink(),
TaskListLoading() => const CircularProgressIndicator(),
TaskListLoaded(tasks: final tasks) => ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return Dismissible(
background: Container(color: Colors.red),
key: ValueKey(tasks[index].id),
onDismissed: (_) {
context.read<TaskListCubit>().deleteTask(
tasks[index].id,
);
},
child: CheckboxListTile(
value: tasks[index].completed,
title: Text(tasks[index].title),
subtitle: Text(tasks[index].description),
onChanged: tasks[index].completed
? null
: (value) {
context
.read<TaskListCubit>()
.markTasksAsCompleted([tasks[index].id]);
},
),
);
},
),
TaskListError(message: final message) => Text(message),
_ => throw Exception("Invalid state"),
};
},
),
),
),
),
);
}
}
class _TaskAdd extends StatefulWidget {
const _TaskAdd();
@override
State<_TaskAdd> createState() => __TaskAddState();
}
class __TaskAddState extends State<_TaskAdd> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -0,0 +1 @@
export 'libsql_task_repository.dart';

View File

@@ -0,0 +1,58 @@
import 'dart:async';
import 'package:path_provider/path_provider.dart';
import 'package:turso_dart/turso_dart.dart';
import 'package:turso_dart_example/features/task/models/task.dart';
import 'package:turso_dart_example/features/task/repositories/repositories.dart';
import 'package:watcher/watcher.dart';
class LibsqlTaskRepository extends TaskRepository {
final LibsqlClient _client;
LibsqlTaskRepository(this._client);
@override
Future<void> addTask(Task task) async {
await _client.execute(
"insert into tasks (title, description, completed) values (?, ?, ?)",
positional: [task.title, task.description, task.completed ? 1 : 0],
);
}
@override
Future<void> deleteTask(int id) async {
await _client.execute("delete from tasks where id = ?", positional: [id]);
}
@override
Future<List<Task>> getTasks() async {
return _client
.query("select * from tasks")
.then(
(value) => value
.map(
(row) => Task(
id: row["id"],
title: row["title"],
description: row["description"],
completed: row["completed"] == 1,
),
)
.toList(),
);
}
@override
Future<void> markTasksAsCompleted(List<int> ids) async {
await _client.execute(
"update tasks set completed = 1 where id in (${ids.join(",")})",
);
}
@override
Future<Stream?> replicaChanges() async {
if (_client.url == ":memory:") return null;
final dir = await getApplicationCacheDirectory();
return DirectoryWatcher(dir.path).events;
}
}

View File

@@ -1,11 +1,31 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:turso_dart/turso_dart.dart';
import 'package:turso_dart_example/bootstrap.dart';
import 'package:turso_dart_example/features/task/repositories/task_repository.dart';
import 'package:turso_dart_example/features/task/task_list.dart';
import 'package:turso_dart_example/infra/libsql_task_repository.dart';
late LibsqlClient memoryClient;
late LibsqlClient localClient;
Future<void> main() async {
final c = LibsqlClient.memory();
await c.connect();
final res = await c.query("SELECT 1");
print(res);
WidgetsFlutterBinding.ensureInitialized();
final dir = await getApplicationCacheDirectory();
await dir.delete(recursive: true);
await dir.create(recursive: true);
// memoryClient = LibsqlClient(":memory:");
memoryClient = LibsqlClient.memory();
// localClient = LibsqlClient("${dir.path}/local.db");
localClient = LibsqlClient.local("${dir.path}/local.db");
await bootstrapDatabase(memoryClient);
await bootstrapDatabase(localClient);
runApp(const MyApp());
}
@@ -16,7 +36,49 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
appBar: AppBar(title: const Text('Libsql Dart Example')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Builder(
builder: (context) {
return Center(
child: Column(
spacing: 16,
children: [
FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Provider<TaskRepository>(
create: (context) =>
LibsqlTaskRepository(memoryClient),
child: const TaskList(),
),
),
);
},
child: const Text("Memory"),
),
FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Provider<TaskRepository>(
create: (context) =>
LibsqlTaskRepository(localClient),
child: const TaskList(),
),
),
);
},
child: const Text("Local"),
),
],
),
);
},
),
),
),
);
}

View File

@@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View File

@@ -1,20 +1,27 @@
PODS:
- FlutterMacOS (1.0.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- turso_dart (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- turso_dart (from `Flutter/ephemeral/.symlinks/plugins/turso_dart/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
turso_dart:
:path: Flutter/ephemeral/.symlinks/plugins/turso_dart/macos
SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
turso_dart: 4b56bcc63567d3acd1509ce808814ab995c537cb
PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82

View File

@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@@ -73,6 +81,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
@@ -86,6 +102,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_driver:
dependency: transitive
description: flutter
@@ -194,14 +218,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
@@ -226,6 +306,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.3"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
sky_engine:
dependency: transitive
description: flutter
@@ -310,6 +398,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: "direct main"
description:
name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
web:
dependency: transitive
description:
@@ -326,6 +422,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.27.0"

View File

@@ -1,99 +1,35 @@
name: turso_dart_example
description: "Demonstrates how to use the turso_dart plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ^3.8.1
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
turso_dart:
# When depending on this package from a real application you should use:
# turso_dart: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_bloc: ^8.1.6
provider: ^6.1.2
path: ^1.9.0
path_provider: ^2.1.4
watcher: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
integration_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package