Skip to content

Commit 08667a0

Browse files
bentokalfonsogarciacaro
authored andcommitted
Flutter state in F#
1 parent 801441c commit 08667a0

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
2+
![fhsarp loves dart](../../static/img/blog/fsharp_loves_dart.png)
3+
4+
# Flutter State in F# with Fable and Redux
5+
6+
[Fable 4.0 is on the horizon](https://fable.io/blog/2022/2022-06-06-Snake_Island_alpha.html) 🎉 and with it comes the ability to compile F# code to Dart. Although Fable 4 is still pre-release, the Dart compilation works great - so great that I was able to migrate the entirety of my Redux state management in a Flutter app over to F#. Being able to model data and dispatch actions in F# has made Flutter so much nicer to work with, so in this post I'll be sharing info about how to do the same approach in your own Flutter app.
7+
8+
I'd like to note up front that my approach here uses only the Fable 4 built-in Dart bindings, and I kept my Flutter widgets in Dart for now. It is possible write full-on Flutter widgets, as seen in [this repo](https://github.com/alfonsogarciacaro/fable-flutterapp), however for now it does require copying over the bindings from that repo at least until 4.0 is officially released. However, in my case I already had a sizable app that would have required making bindings for dozens of libraries and I was contrained on time. Since my Redux architecture did not involve any libraries and modelling state is a particularly verbose process in Dart, I determined that state would be a great place to start and that I could port the rest to F# later.
9+
10+
## Elegant Models in F#
11+
Flutter Redux is a pretty lightweight library in which you can write actions and reducers in plain Dart classes, combine them into a single app reducer, create a store which uses that reducer and use that store to update the state of all descendent widgets. I won't go into how to use Flutter Redux (here's a good article on that), other than covering important information with regards to using F# with it.
12+
For the most part, using Redux in Flutter can be written in pure Dart - which means that with Fable you can write that same code in F# with no need for additional Fable bindings. Consider this user model:
13+
14+
```Dart
15+
import 'package:flutter/material.dart';
16+
17+
@immutable
18+
class UserModel {
19+
final String firstName;
20+
final String lastName;
21+
final String email;
22+
23+
const UserModel({
24+
this.firstName = '',
25+
this.lastName = '',
26+
this.email = '',
27+
});
28+
29+
@override
30+
bool operator ==(Object other) =>
31+
identical(this, other) ||
32+
other is ClassSliceModel &&
33+
runtimeType == other.runtimeType &&
34+
firstName == other.lastName &&
35+
lastName == other.lastName &&
36+
email == other.email;
37+
38+
@override
39+
int get hashCode =>
40+
firstName.hashCode ^
41+
lastname.hashCode ^
42+
email.hashCode;
43+
}
44+
```
45+
46+
Now with Fable, we can write the same code as follows:
47+
48+
```F#
49+
type UserModel =
50+
{ firstName: string
51+
lastName: string
52+
email: string }
53+
```
54+
55+
The above compiles to the same original Dart code (minus the @immutable decorator from Material). The compiled file `user_model.fs`.dart can now be imported and used in a Dart file just like the original model.
56+
57+
## Actions
58+
59+
In Flutter Redux, actions also can be written in pure Dart which, again, means that we'll have no problem writing the same thing in F#. Given the UserModel class above, here is what the Redux actions might look like.
60+
61+
```Dart
62+
class FirstNameChanged {
63+
String firstName;
64+
65+
FirstNameChanged(this.firstName);
66+
}
67+
68+
class LastNameChanged {
69+
String lastName;
70+
71+
LastNameChanged(this.lastName);
72+
}
73+
74+
class EmailChanged {
75+
String email;
76+
77+
EmailChanged(this.email);
78+
}
79+
```
80+
81+
The same in F#:
82+
83+
```F#
84+
type UserActions =
85+
| FirstNameChanged of string
86+
| LastNameChanged of string
87+
| EmailChanged of string
88+
```
89+
90+
## Reducers
91+
92+
Reducers in Dart tend to be large if/else statements, or maybe switch statements, which check for the type of the action and look like this:
93+
94+
```Dart
95+
UserModel userReducer(UserModel state, dynamic action) {
96+
if (action is FirstNameChanged) {
97+
return UserModel(
98+
firstName: action.firstName,
99+
lastName: state.lastName,
100+
email: state.email,
101+
);
102+
}
103+
if (action is LastNameChanged) {
104+
return UserModel(
105+
firstName: state.firstName,
106+
lastName: action.lastName,
107+
email: state.email,
108+
);
109+
}
110+
if (action is EmailChanged) {
111+
return UserModel(
112+
firstName: state.firstName,
113+
lastName: state.lastName,
114+
email: action.email,
115+
);
116+
}
117+
return state;
118+
}
119+
```
120+
121+
The same in F#:
122+
123+
```F#
124+
let userReducer (state: AppStateModel, action: UserActions) =
125+
{ state with
126+
user =
127+
match action with
128+
| FirstNameChanged x -> { state.user with firstName = x }
129+
| LastNameChanged x -> { state.user with lastName = x }
130+
| EmailChanged x -> { state.user with email = x } }
131+
132+
```
133+
134+
One other thing to note is that, while Dart could have independent, modular reducers because of the `dynamic` type, in F# we'll need to have a single global discriminated union to allow us to pattern match on which reducer within the global state we need to dispatch to. So we'll also have a reducer that looks like this for the whole app:
135+
136+
```F#
137+
type AppActions =
138+
| UserDispatch of UserActions
139+
140+
let appReducer (state, action) =
141+
match action with
142+
| UserDispatch action -> userReducer (state, action)
143+
```
144+
145+
## Dispatching Events
146+
147+
Now with the essential elements of a Redux state converted from Dart to F#, we can look at how to dispatch events. Before with Dart, we'd dispatch an event by importing the Action class directly like so:
148+
149+
```Dart
150+
store.dispatch(FirstNameChanged('Bob'));
151+
```
152+
153+
The F# code compiles disciminated unions to functions that are namespaced by the type's name. So `FirstNameChanged` on the UserActions discriminated union compiles to `UserActions_FirstNameChanged`. But also remember that the root reducer is now a monolithic state instead of a modular one, so we also need to dispatch the top-level reducer's action. Given the appReducer above, it would look like this `AppActions_UserDispatch(UserActions_FirstNameChanged('Bob'))`.
154+
155+
That is a bit verbose though, so we can create functions in F# that compose the correct functions together in a friendlier name:
156+
157+
```F#
158+
let firstNameChanged =
159+
UserDispatch << FirstNameChanged
160+
```
161+
162+
Now in our Dart code we can dispatch our actions by importing the function we just wrote in F# with `store.dispatch(firstNameChanged('Bob'))`.
163+
164+
## Selectors
165+
166+
Having all of this code in F# is already making state management much more pleasant, but there's a cherry on top as well. Although Dart has some nice built in utilities for collections, now you have access to wonderful FShare.Core modules such as the List module. So now you can create a UserSelectors module to grab data from the store and easily shape it - stuff like this:
167+
168+
```Dart
169+
selectUsersGroupedByCompany(state)
170+
.map((e) => buildSomeWidget(e));
171+
```
172+
173+
Where `selectUsersGroupedByCompany` is written in F# like so:
174+
175+
```F#
176+
let selectUsersGroupedByCompany state =
177+
state.users
178+
|> List.groupBy (fun u -> u.Company)
179+
```
180+
181+
Although `groupBy` isn't a great example because you can do it fairly tersely in Dart, a lot of more verbose Dart code can now be written written in F# utilities.
182+
183+
## Conclusion
184+
185+
Fable 4 now gives us the ability to compile F# to Dart, and it's been an incredibly smooth process for me, even with it only just now reaching beta. In my case, I have a hybrid application with both Dart code and F# code. The most expedient approach for me for now was to only write my state in F# and much of the most imporant logic ended up there, while I left the dependency-heavy widgets in Dart for now. Interacting with the compiled code from Dart files has been pleasant as well.
186+
187+
Despite only having partially written my app with Fable, I look forward to eventually writing everything with it (I also would like to try writing a game in Flutter and F#). If you are a Dart or Flutter developer who loves F#, you should definitely give it a try.
47.1 KB
Loading

0 commit comments

Comments
 (0)