Skip to content

Commit

Permalink
feat/1.0.0 (#61)
Browse files Browse the repository at this point in the history
Co-authored-by: Luke Greenwood <[email protected]>
Co-authored-by: manuel-plavsic <[email protected]>
  • Loading branch information
3 people authored Aug 4, 2023
1 parent 13a5c92 commit c4159eb
Show file tree
Hide file tree
Showing 118 changed files with 6,064 additions and 2,945 deletions.
32 changes: 17 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
push:
branches:
- main
- dev
paths-ignore:
- "**.md"

Expand All @@ -19,34 +20,35 @@ jobs:

steps:
- uses: actions/checkout@v3
- uses: dart-lang/setup-dart@v1.3
- uses: dart-lang/setup-dart@v1.5.0

# Dart
- name: Install dependencies
run: dart pub get
working-directory: ./packages/solidart
- name: Analyze project source
run: dart analyze
working-directory: ./packages/solidart

# Flutter
- uses: subosito/flutter-action@v2
- uses: subosito/flutter-action@v2.10.0
with:
channel: "stable"
- name: Install dependencies
run: flutter pub get
working-directory: ./packages/flutter_solidart
- name: Analyze project source
run: flutter analyze
working-directory: ./packages/flutter_solidart

# Coverage
- name: Generate code coverage
run: |
chmod +x scripts/coverage.sh
./scripts/coverage.sh
shell: bash
working-directory: ./
- name: Install melos
run: dart pub global activate melos

- name: Install VeryGoodCLI
run: dart pub global activate very_good_cli

- name: Analyze packages
run: melos run analyze

- name: Test packages
run: melos run test

- name: Combine coverage
run: melos run combine_coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ coverage/
lcov.info
.fvm
.vscode/settings.json
.flutter-plugins-dependencies
.flutter-plugins
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
"request": "launch",
"program": "lib/main.dart",
"cwd": "examples/toggle_theme/"
},
{
"name": "Github search",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"cwd": "examples/github_search/"
}
]
}
143 changes: 81 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The objectives of this project are:
1. Being simple and easy to learn
2. Do not go against the framework (e.g. Flutter) with weird workarounds.
3. Do not have a single global state, but put multiple states only in the most appropriate places
4. No code generation

## Learning

Expand All @@ -34,6 +35,8 @@ To create a signal, you have to use the `createSignal` method:

```dart
final counter = createSignal(0);
// or
final counter = Signal(0);
```

The argument passed to the create call is the initial value, and the return value is the signal.
Expand All @@ -49,6 +52,8 @@ counter.value++; // Increments by 1
// or
counter.value = 2; // Sets the value to 2
// or
counter.set(3);
// or
counter.update((value) => value * 2); // Update the value based on the current value
```

Expand All @@ -68,14 +73,17 @@ SignalBuilder(
Signals are trackable values, but they are only one half of the equation. To complement those are observers that can be updated by those trackable values. An effect is one such observer; it runs a side effect that depends on signals.

An effect can be created by using `createEffect`.
The effect subscribes to any signal provided in the `signals` array and reruns when any of them change.

The effect automatically subscribes to any signal and reruns when any of them change.
So let's create an Effect that reruns whenever `counter` changes:

```dart
createEffect(() {
final disposeFn = createEffect((_) {
print("The count is now ${counter.value}");
}, signals: [counter]);
});
// or
final effect = Effect((disposeFn) {
print("The count is now ${counter.value}");
});
```

### Resources
Expand All @@ -102,39 +110,52 @@ Future<String> fetchUser() async {
// The resource
final user = createResource(fetcher: fetchUser, source: userId);
// or
final user = Resource(fetcher: fetchUser, source: userId);
```

A Resource can also be driven from a [stream] instead of a Future.
In this case you just need to pass the `stream` field to the `createResource` method.
The [source] field is ignored for the [stream] and used only for a [fetcher].

If you're using `ResourceBuilder` you can react to the state of the resource:

```dart
ResourceBuilder(
resource: user,
builder: (_, resource) {
return resource.on(
// the call was successful
ready: (data, refreshing) {
builder: (_, userState) {
return userState.on(
ready: (data) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(data),
subtitle: Text('refreshing: $refreshing'),
),
ElevatedButton(
// you can refetch if you want to update the data
onPressed: user.refetch,
child: const Text('Refresh'),
subtitle:
Text('refreshing: ${userState.isRefreshing}'),
),
userState.isRefreshing
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: user.refresh,
child: const Text('Refresh'),
),
],
);
},
error: (e, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(e.toString()),
userState.isRefreshing
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: user.refresh,
child: const Text('Refresh'),
),
],
);
},
// the call failed.
error: (e, _) => Text(e.toString()),
// the call is loading.
loading: () {
return const RepaintBoundary(
child: CircularProgressIndicator(),
Expand Down Expand Up @@ -166,37 +187,30 @@ Let's see an example to grasp the concept.
You're going to see how to build a toggle theme feature using `Solid`, this example is present also here https://github.com/nank1ro/solidart/tree/main/examples/toggle_theme

```dart
/// The identifiers used for [Solid] signals.
enum SignalId { // [1]
themeMode,
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Provide the theme mode signal to descendats
return Solid( // [2]
signals: {
// the id of the signal and the signal associated.
SignalId.themeMode: () => createSignal<ThemeMode>(ThemeMode.light),
return Solid( // [1]
providers: [
SolidSignal<Signal<ThemeMode>>(
create: () => createSignal(ThemeMode.light),
),
],
// using the builder method to immediately access the signal
builder: (context) {
// observe the theme mode value this will rebuild every time the themeMode signal changes.
final themeMode = context.observe<ThemeMode>(); // [2]
return MaterialApp(
title: 'Toggle theme',
themeMode: themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const MyHomePage(),
);
},
child:
// using a builder here because the `context` must be a descendant of [Solid]
Builder(
builder: (context) {
// observe the theme mode value this will rebuild every time the themeMode signal changes.
final themeMode = context.observe<ThemeMode>(SignalId.themeMode); // [3]
return MaterialApp(
title: 'Toggle theme',
themeMode: themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const MyHomePage(),
);
},
),
);
}
}
Expand All @@ -207,19 +221,19 @@ class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// retrieve the theme mode signal
final themeMode = context.get<Signal<ThemeMode>>(SignalId.themeMode); // [4]
final themeMode = context.get<Signal<ThemeMode>>(); // [3]
return Scaffold(
appBar: AppBar(
title: const Text('Toggle theme'),
),
body: Center(
child:
// Listen to the theme mode signal rebuilding only the IconButton
SignalBuilder( // [5]
SignalBuilder( // [4]
signal: themeMode,
builder: (_, mode, __) {
return IconButton(
onPressed: () { // [6]
onPressed: () { // [5]
// toggle the theme mode
if (themeMode.value == ThemeMode.light) {
themeMode.value = ThemeMode.dark;
Expand All @@ -239,28 +253,33 @@ class MyHomePage extends StatelessWidget {
}
```

In this example many things occured, first at `[1]` we've used an _Enum_ to store all the [SignalId]s.
You may use a `String`, an `int` or wethever you want. Just be sure to use the same id to retrieve the signal.

Then at `[2]` we've used the `Solid` widget to provide the `themeMode` signal to descendants.

The `Solid` widgets takes a Map of `signals`:
First at `[1]` we've used the `Solid` widget to provide the `themeMode` signal to descendants.

- The key of the Map is the signal id, in this case `SignalId.themeMode`.
- The value of the Map is a function that returns a `SignalBase`. You may create a signal or a derived signal. The value is a Function because the signal is created lazily only when used for the first time, if you never access the signal it never gets created.
The `Solid` widgets takes a list of providers:
The `SolidSignal` has a `create` function that returns a `SignalBase`.
You may create a signal or a derived signal. The value is a Function
because the signal is created lazily only when used for the first time, if
you never access the signal it never gets created.
In the `SolidSignal` you can also specify an `id`entifier for having multiple
signals of the same type.

At `[3]` we `observe` the value of a signal. The `observe` method listen to the signal value and rebuilds the widget when the value changes. It takes an `id` that is the signal identifier that you want to use. This method must be called only inside the `build` method.
At `[2]` we `observe` the value of a signal. The `observe` method listen to the signal value and rebuilds the widget when the value changes. It takes an optional `id` that is the signal identifier that you want to use. This method must be called only inside the `build` method.

At `[4]` we `get` the signal with the given `id`. This doesn't listen to signal value. You may use this method inside the `initState` and `build` methods.
At `[3]` we `get` the signal with the given signal type. This doesn't listen to signal value. You may use this method inside the `initState` and `build` methods.

At `[5]` using the `SignalBuilder` widget we rebuild the `IconButton` every time the signal value changes.
At `[4]` using the `SignalBuilder` widget we rebuild the `IconButton` every time the signal value changes.

And finally at `[6]` we update the signal value.
And finally at `[5]` we update the signal value.

> It is mandatory to pass the type of signal value otherwise you're going to encounter an error, for example:
1. `createSignal<ThemeMode>` and `context.observe<ThemeMode>` where ThemeMode is the type of the signal value.
2. `context.get<Signal<ThemeMode>>` where `Signal<ThemeMode>` is the type of signal with its type value.
```dart
SolidSignal<Signal<ThemeMode>>(create: () => createSignal(ThemeMode.light))
```
and `context.observe<ThemeMode>` where ThemeMode is the type of the signal
value.
`context.get<Signal<ThemeMode>>` where `Signal<ThemeMode>` is the type
of signal with its type value.

## Examples

Expand All @@ -269,6 +288,7 @@ And finally at `[6]` we update the signal value.
- [Counter](https://zapp.run/github/nank1ro/solidart/tree/main/examples/counter)
- [Toggle theme (dark/light mode)](https://zapp.run/github/nank1ro/solidart/tree/main/examples/toggle_theme)
- [Todos](https://zapp.run/github/nank1ro/solidart/tree/main/examples/todos)
- [Github Search](https://zapp.run/github/nank1ro/solidart/tree/main/examples/github_search)

### Showcase of all flutter_solidart features

Expand All @@ -278,12 +298,11 @@ Learn every feature of `flutter_solidart` including:

1. `createSignal`
2. `Show` widget
3. Derived signals with `signal.select()`
4. `Effect`s with basic and advanced usages
3. Derived signals with `createComputed`
4. `Effect`s
5. `SignalBuilder`, `DualSignalBuilder` and `TripleSignalBuilder`
6. `createResource` and `ResourceBuilder`
7. `Solid` and its fine-grained reactivity
8. Access signals in modals with `Solid.value`

## Contributors

Expand Down
Loading

0 comments on commit c4159eb

Please sign in to comment.