getx_plus 5.0.0
getx_plus: ^5.0.0 copied to clipboard
Manage states, inject dependencies, and navigate without context easily with GetX Plus.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:getx_plus/get.dart';
import 'stress_test_page.dart';
// ══════════════════════════════════════════════════════════
// 服务 — GetxService (永久实例,不会被自动回收)
// ══════════════════════════════════════════════════════════
class AuthService extends GetxService {
final username = 'Flutter Dev'.obs;
final isLoggedIn = true.obs;
Future<AuthService> init() async {
// 模拟异步初始化(网络请求、读取 token 等)
await Future.delayed(const Duration(milliseconds: 300));
Get.log('AuthService initialized');
return this;
}
}
// ══════════════════════════════════════════════════════════
// 控制器 — GetxController + 响应式 & Workers
// ══════════════════════════════════════════════════════════
class CounterController extends GetxController {
final count = 0.obs;
final history = <String>[].obs;
late Worker _everWorker;
late Worker _debounceWorker;
late Worker _intervalWorker;
@override
void onInit() {
super.onInit();
// ever: 每次 count 变化都触发
_everWorker = ever(count, (val) {
history.add('[ever] count=$val');
});
// debounce: 停止变化 500ms 后触发
_debounceWorker = debounce(count, (val) {
history.add('[debounce] settled at $val');
}, time: const Duration(milliseconds: 500));
// interval: 最多每 1s 触发一次
_intervalWorker = interval(count, (val) {
history.add('[interval] sampled $val');
}, time: const Duration(seconds: 1));
}
void increment() => count.value++;
void decrement() => count.value--;
void reset() => count.value = 0;
@override
void onClose() {
_everWorker.dispose();
_debounceWorker.dispose();
_intervalWorker.dispose();
super.onClose();
}
}
// ══════════════════════════════════════════════════════════
// 控制器 — 使用 update() 的经典模式 (GetBuilder)
// ══════════════════════════════════════════════════════════
class TodoController extends GetxController {
final todos = <String>[];
final textCtrl = TextEditingController();
void addTodo() {
final text = textCtrl.text.trim();
if (text.isNotEmpty) {
todos.add(text);
textCtrl.clear();
update(); // 通知 GetBuilder 刷新
}
}
void removeTodo(int index) {
todos.removeAt(index);
update(); // 通知 GetBuilder 刷新
}
@override
void onClose() {
textCtrl.dispose();
super.onClose();
}
}
// ══════════════════════════════════════════════════════════
// 控制器 — ScrollMixin 演示无限滚动
// ══════════════════════════════════════════════════════════
class InfiniteListController extends GetxController with ScrollMixin {
final items = <int>[].obs;
final isLoading = false.obs;
@override
void onInit() {
super.onInit();
_loadMore();
}
Future<void> _loadMore() async {
isLoading.value = true;
await Future.delayed(const Duration(milliseconds: 500));
final start = items.length;
items.addAll(List.generate(20, (i) => start + i));
isLoading.value = false;
}
@override
Future<void> onEndScroll() async {
await _loadMore();
}
@override
Future<void> onTopScroll() async {
Get.log('Scrolled to top');
}
}
// ══════════════════════════════════════════════════════════
// 控制器 — 设置页,演示主题切换
// ══════════════════════════════════════════════════════════
class SettingsController extends GetxController {
final isDark = false.obs;
void toggleTheme() {
isDark.toggle();
Get.changeThemeMode(isDark.value ? ThemeMode.dark : ThemeMode.light);
}
}
// ══════════════════════════════════════════════════════════
// 路由中间件 — GetMiddleware
// ══════════════════════════════════════════════════════════
class LogMiddleware extends GetMiddleware {
@override
int get priority => 0;
@override
GetPage? onPageCalled(GetPage? page) {
Get.log('🔀 Middleware: navigating to ${page?.name}');
return page;
}
}
class AuthMiddleware extends GetMiddleware {
@override
int get priority => 1;
@override
RouteSettings? redirect(String? route) {
final auth = Get.find<AuthService>();
if (!auth.isLoggedIn.value) {
return const RouteSettings(name: '/');
}
return null; // 允许通过
}
}
// ══════════════════════════════════════════════════════════
// Bindings
// ══════════════════════════════════════════════════════════
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
Get.lazyPut(() => TodoController());
Get.lazyPut(() => SettingsController());
}
}
class InfiniteListBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => InfiniteListController());
}
}
// ══════════════════════════════════════════════════════════
// App 入口
// ══════════════════════════════════════════════════════════
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 预先注册永久服务 (Get.put permanent)
await Get.put(AuthService(), permanent: true).init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
enableLog: true,
smartManagement: SmartManagement.full,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.light,
routingCallback: (routing) {
// 全局路由监听
if (routing != null) {
Get.log('📍 Route: ${routing.current}');
}
},
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => const HomePage(),
binding: HomeBinding(),
transition: Transition.fadeIn,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/reactive',
page: () => const ReactiveDemoPage(),
transition: Transition.rightToLeftWithFade,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/workers',
page: () => const WorkerDemoPage(),
transition: Transition.zoom,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/todos',
page: () => const TodoPage(),
transition: Transition.downToUp,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/infinite',
page: () => const InfiniteListPage(),
binding: InfiniteListBinding(),
transition: Transition.cupertino,
middlewares: [LogMiddleware(), AuthMiddleware()],
),
GetPage(
name: '/settings',
page: () => const SettingsPage(),
transition: Transition.leftToRight,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/detail/:id',
page: () => const DetailPage(),
transition: Transition.circularReveal,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/di',
page: () => const DiDemoPage(),
transition: Transition.size,
middlewares: [LogMiddleware()],
),
GetPage(
name: '/nav',
page: () => const NavDemoPage(),
transition: Transition.fade,
middlewares: [LogMiddleware()],
children: [
GetPage(
name: '/child',
page: () => const NavChildPage(),
transition: Transition.upToDown,
),
],
),
GetPage(name: '/stress', page: () => const StressTestPage()),
],
unknownRoute: GetPage(name: '/404', page: () => const NotFoundPage()),
);
}
}
// ══════════════════════════════════════════════════════════
// 首页 — 导航入口 + GetView 演示
// ══════════════════════════════════════════════════════════
class HomePage extends GetView<CounterController> {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text('GetX Plus 全功能演示 (${controller.count})')),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => Get.toNamed('/settings'),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [..._buildMenuItems()],
),
// Obx 自动追踪 count 变化
floatingActionButton: Obx(
() => FloatingActionButton.extended(
onPressed: controller.increment,
icon: const Icon(Icons.add),
label: Text('${controller.count}'),
),
),
);
}
List<Widget> _buildMenuItems() {
final items = [
('响应式演示', Icons.bolt, '/reactive'),
('Worker 演示', Icons.timer, '/workers'),
('待办事项', Icons.checklist, '/todos'),
('路由演示', Icons.navigation, '/nav'),
('依赖注入', Icons.hub, '/di'),
('Infinite Scroll', Icons.all_inclusive, '/infinite'),
('⚡ 性能压力测试', Icons.speed, '/stress'),
];
return items.map((e) {
final (label, icon, route) = e;
return Card(
child: ListTile(
leading: Icon(icon),
title: Text(label),
trailing: const Icon(Icons.chevron_right),
onTap: () => Get.toNamed(route),
),
);
}).toList();
}
}
// ══════════════════════════════════════════════════════════
// 响应式演示 — Rx 各种类型 + Obx / ObxValue
// ══════════════════════════════════════════════════════════
class ReactiveDemoPage extends StatelessWidget {
const ReactiveDemoPage({super.key});
@override
Widget build(BuildContext context) {
// 局部响应式变量(不需要控制器)
final name = 'Alice'.obs;
final score = 99.5.obs;
final isActive = true.obs;
final tags = <String>{'flutter', 'dart'}.obs;
final scores = <String, int>{'math': 100, 'eng': 95}.obs;
final items = <int>[1, 2, 3].obs;
return Scaffold(
appBar: AppBar(title: const Text('响应式演示')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// RxString
_section('RxString'),
Obx(
() => Text('Name: ${name.value} (length: ${name.value.length})'),
),
ElevatedButton(
onPressed: () =>
name.value = name.value == 'Alice' ? 'Bob' : 'Alice',
child: const Text('Toggle Name'),
),
const Divider(),
// RxDouble
_section('RxDouble'),
Obx(() => Text('Score: ${score.value.toStringAsFixed(1)}')),
Obx(
() => Slider(
value: score.value.clamp(0.0, 100.0),
min: 0,
max: 100,
onChanged: (v) => score.value = v,
),
),
const Divider(),
// RxBool + toggle()
_section('RxBool'),
Obx(
() => SwitchListTile(
title: Text('Active: ${isActive.isTrue ? "YES" : "NO"}'),
value: isActive.value,
onChanged: (_) => isActive.toggle(),
),
),
const Divider(),
// RxList
_section('RxList'),
Obx(() => Text('Items: $items')),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => items.add(items.length + 1),
child: const Text('Add'),
),
ElevatedButton(
onPressed: () {
if (items.isNotEmpty) items.removeLast();
},
child: const Text('Remove Last'),
),
],
),
const Divider(),
// RxSet
_section('RxSet'),
Obx(() => Text('Tags: ${tags.join(", ")}')),
ElevatedButton(
onPressed: () =>
tags.contains('getx') ? tags.remove('getx') : tags.add('getx'),
child: const Text('Toggle "getx" tag'),
),
const Divider(),
// RxMap
_section('RxMap'),
Obx(() => Text('Scores: $scores')),
ElevatedButton(
onPressed: () => scores['science'] = (scores['science'] ?? 0) + 10,
child: const Text('Add 10 to Science'),
),
const Divider(),
// ObxValue — 局部状态管理小组件
_section('ObxValue<RxBool>'),
ObxValue<RxBool>(
(data) => CheckboxListTile(
title: const Text('Agree to terms'),
value: data.value,
onChanged: (_) => data.toggle(),
),
false.obs,
),
],
),
);
}
Widget _section(String title) => Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
// ══════════════════════════════════════════════════════════
// Worker 演示 — ever / debounce / interval / once
// ══════════════════════════════════════════════════════════
class WorkerDemoPage extends GetView<CounterController> {
const WorkerDemoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Worker 演示')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton.filled(
onPressed: controller.decrement,
icon: const Icon(Icons.remove),
),
Obx(() => Text('${controller.count}')),
IconButton.filled(
onPressed: controller.increment,
icon: const Icon(Icons.add),
),
],
),
),
ElevatedButton(
onPressed: controller.reset,
child: const Text('Reset'),
),
const Padding(
padding: EdgeInsets.all(8),
child: Text(
'Worker History:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Obx(
() => ListView.builder(
itemCount: controller.history.length,
itemBuilder: (_, i) {
final idx = controller.history.length - 1 - i;
return ListTile(
dense: true,
leading: const Icon(Icons.arrow_right, size: 16),
title: Text(
controller.history[idx],
style: const TextStyle(fontSize: 13),
),
);
},
),
),
),
],
),
);
}
}
// ══════════════════════════════════════════════════════════
// Todo 页面 — GetBuilder + update() 经典模式
// ══════════════════════════════════════════════════════════
class TodoPage extends GetView<TodoController> {
const TodoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('待办事项')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.textCtrl,
decoration: InputDecoration(
hintText: '添加待办',
border: const OutlineInputBorder(),
),
onSubmitted: (_) => controller.addTodo(),
),
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: controller.addTodo,
icon: const Icon(Icons.add),
),
],
),
),
Expanded(
// GetBuilder: 手动 update() 驱动刷新,支持 id 过滤
child: GetBuilder<TodoController>(
builder: (ctrl) {
if (ctrl.todos.isEmpty) {
return Center(child: Text('暂无待办事项'));
}
return ListView.builder(
itemCount: ctrl.todos.length,
itemBuilder: (_, i) => Dismissible(
key: ValueKey('${ctrl.todos[i]}_$i'),
onDismissed: (_) => ctrl.removeTodo(i),
background: Container(
color: Colors.red.shade100,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete),
),
child: ListTile(
leading: CircleAvatar(child: Text('${i + 1}')),
title: Text(ctrl.todos[i]),
),
),
);
},
),
),
// GetBuilder 全局刷新显示总数
GetBuilder<TodoController>(
builder: (ctrl) => Padding(
padding: const EdgeInsets.all(8),
child: Text('Total: ${ctrl.todos.length}'),
),
),
],
),
);
}
}
// ══════════════════════════════════════════════════════════
// 无限滚动 — ScrollMixin + AuthMiddleware 守卫
// ══════════════════════════════════════════════════════════
class InfiniteListPage extends GetView<InfiniteListController> {
const InfiniteListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Infinite Scroll (ScrollMixin)')),
body: Obx(
() => ListView.builder(
controller: controller.scroll,
itemCount: controller.items.length + 1,
itemBuilder: (_, i) {
if (i == controller.items.length) {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: controller.isLoading.value
? const CircularProgressIndicator()
: const Text('Scroll down for more'),
),
);
}
return ListTile(
leading: CircleAvatar(child: Text('${controller.items[i]}')),
title: Text('Item #${controller.items[i]}'),
);
},
),
),
);
}
}
// ══════════════════════════════════════════════════════════
// 设置页面 — 主题切换
// ══════════════════════════════════════════════════════════
class SettingsPage extends GetView<SettingsController> {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: Obx(
() => ListView(
children: [
SwitchListTile(
title: const Text('切换主题'),
subtitle: Text(controller.isDark.value ? 'Dark' : 'Light'),
secondary: Icon(
controller.isDark.value ? Icons.dark_mode : Icons.light_mode,
),
value: controller.isDark.value,
onChanged: (_) => controller.toggleTheme(),
),
],
),
),
);
}
}
// ══════════════════════════════════════════════════════════
// 依赖注入演示 — put / lazyPut / find / delete / isRegistered
// ══════════════════════════════════════════════════════════
class _DemoService {
final String id;
_DemoService(this.id);
@override
String toString() => 'DemoService($id)';
}
class DiDemoPage extends StatelessWidget {
const DiDemoPage({super.key});
@override
Widget build(BuildContext context) {
final log = <String>[].obs;
void addLog(String msg) => log.add(msg);
return Scaffold(
appBar: AppBar(title: const Text('依赖注入')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () {
Get.put(_DemoService('singleton'), tag: 'demo');
addLog('✅ put _DemoService(singleton) tag=demo');
},
child: const Text('Get.put'),
),
ElevatedButton(
onPressed: () {
Get.lazyPut(() => _DemoService('lazy'), tag: 'lazy');
addLog('✅ lazyPut _DemoService(lazy) tag=lazy');
},
child: const Text('Get.lazyPut'),
),
ElevatedButton(
onPressed: () {
try {
final s = Get.find<_DemoService>(tag: 'demo');
addLog('🔍 find tag=demo → $s');
} catch (e) {
addLog('❌ find tag=demo → not found');
}
},
child: const Text('find(demo)'),
),
ElevatedButton(
onPressed: () {
try {
final s = Get.find<_DemoService>(tag: 'lazy');
addLog('🔍 find tag=lazy → $s');
} catch (e) {
addLog('❌ find tag=lazy → not found');
}
},
child: const Text('find(lazy)'),
),
ElevatedButton(
onPressed: () {
final r = Get.isRegistered<_DemoService>(tag: 'demo');
addLog('📋 isRegistered(demo)=$r');
},
child: const Text('isRegistered'),
),
ElevatedButton(
onPressed: () {
final r = Get.isPrepared<_DemoService>(tag: 'lazy');
addLog('📋 isPrepared(lazy)=$r');
},
child: const Text('isPrepared'),
),
ElevatedButton(
onPressed: () {
final deleted = Get.delete<_DemoService>(tag: 'demo');
addLog('🗑 delete(demo)=$deleted');
},
child: const Text('delete(demo)'),
),
ElevatedButton(
onPressed: () {
// putOrFind: 有则返回,无则注册
final s = Get.putOrFind(() => _DemoService('putOrFind'));
addLog('🔄 putOrFind → $s');
},
child: const Text('putOrFind'),
),
ElevatedButton(
onPressed: () {
// AuthService 是永久的 GetxService
final auth = Get.find<AuthService>();
addLog('🔒 AuthService: ${auth.username}');
},
child: const Text('find AuthService'),
),
],
),
),
const Divider(),
Expanded(
child: Obx(
() => ListView.builder(
reverse: true,
itemCount: log.length,
itemBuilder: (_, i) {
final idx = log.length - 1 - i;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 2,
),
child: Text(log[idx], style: const TextStyle(fontSize: 13)),
);
},
),
),
),
],
),
);
}
}
// ══════════════════════════════════════════════════════════
// 路由演示 — to / toNamed / off / back / arguments / parameters
// ══════════════════════════════════════════════════════════
class NavDemoPage extends StatelessWidget {
const NavDemoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('路由演示')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_navButton('Get.to (anonymous route)', () {
Get.to(
() => const _AnonymousPage(),
transition: Transition.rightToLeft,
duration: const Duration(milliseconds: 400),
);
}),
_navButton('Get.toNamed with arguments', () {
Get.toNamed('/detail/42', arguments: {'title': 'Item 42'});
}),
_navButton('Get.toNamed with query params', () {
Get.toNamed('/detail/7?color=blue&size=large');
}),
_navButton('Get.toNamed → child route', () {
Get.toNamed('/nav/child');
}),
_navButton('Get.offNamed (replace)', () {
Get.offNamed('/reactive');
}),
_navButton('Navigate to unknown → 404', () {
Get.toNamed('/this-does-not-exist');
}),
_navButton('Get.back with result', () {
Get.back(result: 'Hello from NavDemo!');
}),
const Divider(),
Padding(
padding: const EdgeInsets.all(8),
child: Text(
'currentRoute: ${Get.currentRoute}\n'
'previousRoute: ${Get.previousRoute}',
style: const TextStyle(fontSize: 13),
),
),
],
),
);
}
Widget _navButton(String label, VoidCallback onTap) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ElevatedButton(onPressed: onTap, child: Text(label)),
);
}
class NavChildPage extends StatelessWidget {
const NavChildPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Child Route (/nav/child)')),
body: Center(
child: ElevatedButton(
onPressed: () => Get.back(),
child: const Text('Go Back'),
),
),
);
}
}
class _AnonymousPage extends StatelessWidget {
const _AnonymousPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Anonymous Route')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('This page was pushed via Get.to()'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Get.back(),
child: const Text('Back'),
),
],
),
),
);
}
}
// ══════════════════════════════════════════════════════════
// 详情页 — URL 参数 + arguments 演示
// ══════════════════════════════════════════════════════════
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
final id = Get.parameters['id'] ?? 'unknown';
final args = Get.arguments;
final query = Uri.base.queryParameters;
return Scaffold(
appBar: AppBar(title: Text('Detail #$id')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Path parameter id: $id'),
const SizedBox(height: 8),
Text('Arguments: $args'),
const SizedBox(height: 8),
Text('Query parameters: $query'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Get.back(),
child: const Text('Go Back'),
),
],
),
),
);
}
}
// ══════════════════════════════════════════════════════════
// 404 页面
// ══════════════════════════════════════════════════════════
class NotFoundPage extends StatelessWidget {
const NotFoundPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('404')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text('Page Not Found', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Get.offAllNamed('/'),
child: const Text('Back to Home'),
),
],
),
),
);
}
}