liblanis
Pure Dart client for Schulportal Hessen (SPH / Lanis).
Session stack, multi-account storage, optional sqlite3 persistence, injectable secrets, and applet parsers. No Flutter dependency for the core API. Flutter apps can use the Riverpod integration documented below.
Published on pub.dev. Also used as a submodule of lanis-mobile/lanis.
Features
- Full SPH session stack (login, cookies, RSA/AES cryptor, HTML/JSON applet parsers)
**EasyLanisClient**— imperative API without Riverpod (default for scripts, CI, other Dart projects)- Optional Riverpod multi-account registry for Flutter apps
- Optional single SQLite database (file or in-memory)
sharedOverAccountSettingsandaccountSpecificSettingswith typed helpers- Offline applet snapshots table (substitutions & timetable write in v1; API is generic)
- Configurable
StorageManager(caller supplies cache directory)
Install
dart pub add liblanis
Or add liblanis under dependencies in your pubspec.yaml (see pub.dev for the current version).
For Flutter apps that use the Riverpod API, also add:
dependencies:
flutter_riverpod: ^3.0.0
sqlite3_flutter_libs: ^0.5.0 # load native sqlite on mobile
Quick start
Import package:liblanis/easy_client.dart. No ProviderScope or code generation required.
In-memory (tests, quick scripts)
import 'package:liblanis/easy_client.dart';
Future<void> main() async {
final client = EasyLanisClient.inMemory();
final id = await client.accounts.add(
schoolId: 5151,
username: 'student.user',
password: r'...',
);
await client.login(accountId: id);
// Direct parser call
final plan = await client.parsers.substitutions.getHome();
// Cached fetch with offline fallback (same as AppletParser.fetchData)
final response = await client.parsers.calendar.fetch(forceRefresh: true);
if (response.contentStatus == ContentStatus.offline) {
// using cached events
}
client.connection.onStatusChanged.listen((status) {
// ConnectionStatus.connected / disconnected
});
await client.logout();
await client.dispose();
}
Available parsers
Access via client.parsers after login:
| Getter | Applet |
|---|---|
substitutions |
Vertretungsplan |
timetable |
Stundenplan (student) |
calendar |
Kalender |
conversations |
Nachrichten |
lessonsStudent |
Mein Unterricht (student) |
lessonsTeacher |
Mein Unterricht (teacher) |
dataStorage |
Dateispeicher |
studyGroups |
Lerngruppen |
Generic access for smoke tests: client.parsers.forApplet('kalender.php').
Flutter (Riverpod)
Prefer package:liblanis/liblanis.dart when you want the provider-based multi-account API used by lanis-mobile.
On Flutter mobile hosts, initialize sqlite before runApp:
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
// ...
}
Configure and mount
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:liblanis/liblanis.dart';
Future<void> main() async {
final overrides = LanisClient.configure(
databasePath: '/path/to/lanis.db', // null => in-memory
secretStore: MySecureStore(), // required when databasePath != null
documentCacheDirectory: '/path/to/cache', // required for downloads
// httpAdapter: cronetAdapter, // optional
userAgent: 'Lanis-Mobile/v3.7.2+83',
);
runApp(
ProviderScope(
overrides: overrides,
child: const MyApp(),
),
);
}
class MySecureStore implements SecretStore {
// e.g. FlutterSecureStorage
@override
Future<void> write(String key, String value) async { /* ... */ }
@override
Future<String?> read(String key) async => null;
@override
Future<void> delete(String key) async { /* ... */ }
}
Accounts & session
final id = await ref.read(accountsProvider.notifier).add(
schoolId: 5151,
schoolName: 'Example',
username: 'user',
password: 'pass',
);
await ref.read(activeAccountProvider.notifier).select(id);
await ref.read(sessionProvider.notifier).authenticate();
ref.listen(connectionStatusProvider, (prev, next) {
// ConnectionStatus.connected / disconnected
});
Settings
final shared = ref.read(sharedOverAccountSettingsProvider);
shared.setString('theme', 'dark');
final account = ref.read(accountSpecificSettingsProvider);
account.setJsonMap('vertretungsplan.php/filter', {/* ... */});
Applets
final response = await ref.read(substitutionsProvider.future);
if (response.contentStatus == ContentStatus.offline) {
// cached SubstitutionPlan
}
final plan = response.content;
// Or use the long-lived parser + stream:
final parser = ref.read(substitutionsParserProvider);
await parser.fetchData();
parser.stream.listen(/* FetcherResponse<SubstitutionPlan> */);
Document downloads
final storage = ref.read(storageManagerProvider);
if (storage != null) {
final path = await storage.downloadFile(url, filename);
}
Calling download APIs without documentCacheDirectory throws StorageNotConfiguredException.
Configuration rules
| Option | Behavior |
|---|---|
databasePath: null |
In-memory sqlite; SecretStore optional |
databasePath: set |
File DB; **secretStore required** |
documentCacheDirectory: null |
Downloads disabled |
httpAdapter |
Optional Dio adapter injection |
Development
cd liblanis
dart pub get
dart run build_runner build
dart test
dart analyze
License
GPL-3.0 — see LICENSE.
Libraries
- easy_client
- Imperative Lanis client without Riverpod providers.
- liblanis
- Pure Dart Lanis (Schulportal Hessen) client.