lilili_ui_core 0.4.1
lilili_ui_core: ^0.4.1 copied to clipboard
Fluent Modifier/Box chaining, Vue-inspired reactive state (ref/computed/watch), slots (SlotHost/Slot/ScopedSlot), and directives (v-if/v-show/v-model) helpers for flattening Flutter widget trees.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:lilili_ui_core/lilili_ui_core.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('lilili_ui_core example'),
bottom: const TabBar(
tabs: [
Tab(text: 'Modifier'),
Tab(text: 'Reactive'),
Tab(text: 'Slot'),
Tab(text: 'Directives'),
],
),
),
body: TabBarView(
children: [
Builder(
builder: (context) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('旧写法(深层嵌套)', style: TextStyle(fontWeight: FontWeight.bold)),
16.gapH,
_buildNestedCard(context),
24.gapH,
const Text('新写法(Modifier + Box)', style: TextStyle(fontWeight: FontWeight.bold)),
16.gapH,
_buildModifierCard(context),
24.gapH,
const Text('wgtIf / wgtFor', style: TextStyle(fontWeight: FontWeight.bold)),
16.gapH,
..._buildWgtDemo(),
],
),
),
),
const ReactiveDemoTab(),
const SlotDemoTab(),
const DirectivesDemoTab(),
],
),
),
),
);
}
void _showTapped(BuildContext context, String which) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text('点击了$which'),
duration: const Duration(seconds: 1),
));
}
Widget _buildNestedCard(BuildContext context) {
return GestureDetector(
onTap: () => _showTapped(context, '旧写法卡片'),
child: Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: const Padding(
padding: EdgeInsets.all(16),
child: Align(
alignment: Alignment.centerLeft,
child: Text('这是旧的嵌套写法'),
),
),
),
);
}
Widget _buildModifierCard(BuildContext context) {
return const Text('这是新的 Modifier 写法').modifier(
const Modifier()
.onTap(() => _showTapped(context, '新写法卡片'))
.background(Colors.blue.shade50, borderRadius: BorderRadius.circular(12))
.padding(16)
.align(Alignment.centerLeft),
);
}
List<Widget> _buildWgtDemo() {
const items = ['苹果', '香蕉', '橙子'];
const loggedIn = true;
return [
wgtIf(loggedIn, const Text('已登录,显示水果列表:')),
8.gapH,
...wgtFor(items, (item, i) => Text('$i. $item')),
];
}
}
class CounterStore {
final count = ref(0);
late final doubled = computed(() => count.value * 2);
}
final _store = CounterStore();
class ReactiveDemoTab extends StatefulWidget {
const ReactiveDemoTab({super.key});
@override
State<ReactiveDemoTab> createState() => _ReactiveDemoTabState();
}
class _ReactiveDemoTabState extends State<ReactiveDemoTab> {
final _log = ref(<String>[]);
StopHandle? _stopWatch;
@override
void initState() {
super.initState();
_stopWatch = watch<int>(_store.count, (n, o) {
_log.value = [..._log.value, 'count: $o -> $n'];
});
}
@override
void dispose() {
_stopWatch?.call();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Watch(细粒度)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
Watch(() => Text('count = ${_store.count.value}, doubled = ${_store.doubled.value}')),
16.gapH,
const Text('ReactiveWidget(整页)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
const CounterLine(),
16.gapH,
FilledButton(
onPressed: () => _store.count.value++,
child: const Text('count++'),
),
24.gapH,
const Text('watch 日志(新旧值)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
Expanded(
child: Watch(() => ListView(
children: [for (final line in _log.value) Text(line)],
)),
),
],
),
);
}
}
class CounterLine extends ReactiveWidget {
const CounterLine({super.key});
@override
Widget build(BuildContext context) =>
Text('ReactiveWidget 看到的 count = ${_store.count.value}');
}
/// 演示组件:声明 header 具名槽(带 fallback)、默认槽、fruit 作用域槽。
class DemoCard extends StatelessWidget {
const DemoCard({super.key, this.slots = const []});
final List<Widget> slots;
static const _fruits = ['苹果', '香蕉', '橙子'];
@override
Widget build(BuildContext context) {
return SlotHost(
slots: slots,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Slot('header',
fallback: Text('默认标题', style: TextStyle(fontWeight: FontWeight.bold))),
8.gapH,
const Slot.def(),
8.gapH,
...wgtFor(_fruits, (fruit, i) => ScopedSlot<String>('fruit',
data: fruit, fallback: (context, f) => Text('- $f'))),
],
),
),
);
}
}
class SlotDemoTab extends StatelessWidget {
const SlotDemoTab({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('全部用默认内容', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
const DemoCard(),
24.gapH,
const Text('填充具名槽 + 默认槽 + 作用域槽', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
DemoCard(slots: [
const SlotContent(
name: 'header',
child: Text('自定义标题 🎉', style: TextStyle(fontWeight: FontWeight.bold))),
const Text('这行进默认槽'),
SlotContent.builder<String>('fruit',
(context, f) => Text('🍎 $f(作用域槽渲染)')),
]),
],
),
);
}
}
class FormStore {
final name = ref('');
final agreed = ref(false);
final darkMode = ref(false);
final volume = ref(30.0);
final city = ref('北京');
final plan = ref('free');
final showCard = ref(true);
}
final _form = FormStore();
class DirectivesDemoTab extends StatelessWidget {
const DirectivesDemoTab({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('v-show(静态版,setState 驱动)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
const _StaticShowDemo(),
24.gapH,
const Text('v-show(State 保留演示,对比上方的静态驱动)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
Row(children: [
const Text('显示卡片:'),
VSwitch(model: _form.showCard),
]),
Box(
modifier: const Modifier()
.showWhen(_form.showCard)
.background(Colors.orange.shade50, borderRadius: BorderRadius.circular(12))
.padding(16),
child: const _CounterCard(), // 隐藏再显示,计数不丢
),
24.gapH,
const Text('v-model 表单', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
VTextField(model: _form.name, decoration: const InputDecoration(labelText: '姓名')),
8.gapH,
Row(children: [const Text('同意条款'), VCheckbox(model: _form.agreed)]),
Row(children: [const Text('深色模式'), VSwitch(model: _form.darkMode)]),
Row(children: [
const Text('音量'),
Expanded(child: VSlider(model: _form.volume, min: 0, max: 100, divisions: 20)),
]),
Row(children: [
const Text('城市:'),
8.gapW,
VDropdown<String>(model: _form.city, items: const ['北京', '上海', '深圳']),
]),
Row(children: [
VRadio<String>(value: 'free', model: _form.plan), const Text('免费版'),
16.gapW,
VRadio<String>(value: 'pro', model: _form.plan), const Text('专业版'),
]),
24.gapH,
const Text('实时状态(Watch)', style: TextStyle(fontWeight: FontWeight.bold)),
8.gapH,
Watch(() => Text(
'name=${_form.name.value} agreed=${_form.agreed.value} '
'dark=${_form.darkMode.value} volume=${_form.volume.value.toStringAsFixed(0)} '
'city=${_form.city.value} plan=${_form.plan.value}')),
],
),
);
}
}
/// 静态版 .show(bool) 演示:用普通 setState 驱动的 bool 开关 Modifier().show,
/// 对比下方 showWhen 的响应式(ReadonlyRef<bool>)驱动方式。
class _StaticShowDemo extends StatefulWidget {
const _StaticShowDemo();
@override
State<_StaticShowDemo> createState() => _StaticShowDemoState();
}
class _StaticShowDemoState extends State<_StaticShowDemo> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Row(children: [
const Text('我会被静态开关切换显示').modifier(const Modifier().show(_visible)),
8.gapW,
OutlinedButton(
onPressed: () => setState(() => _visible = !_visible),
child: const Text('切换'),
),
]);
}
}
class _CounterCard extends StatefulWidget {
const _CounterCard();
@override
State<_CounterCard> createState() => _CounterCardState();
}
class _CounterCardState extends State<_CounterCard> {
int _n = 0;
@override
Widget build(BuildContext context) {
return Row(children: [
Text('本地计数(State): $_n'),
16.gapW,
OutlinedButton(onPressed: () => setState(() => _n++), child: const Text('+1')),
]);
}
}