Last active
March 11, 2018 13:17
-
-
Save lucamtudor/0cc6a4d101f1ae8ac08e8815104beb81 to your computer and use it in GitHub Desktop.
Flutter: sync redux store with Firebase
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This pattern makes it easier to keep a redux store in sync with Firebase. | |
// I used Firebase firestore, but the same thing applies to the Firebase real-time database. | |
// - Flutter - | |
void main() { | |
runApp(new ShiftStudioApp()); | |
} | |
class ShiftStudioApp extends StatefulWidget { | |
@override | |
State<StatefulWidget> createState() => new ShiftStudioState(); | |
} | |
class ShiftStudioState extends State<ShiftStudioApp> { | |
final appName = "Shift STUDIO Redux Firebase sync"; | |
final store = new Store<AppState>( | |
appReducer, | |
initialState: new AppState(), | |
middleware: createSessionMiddleware(), | |
); | |
@override | |
Widget build(BuildContext context) { | |
return new StoreProvider( | |
store: store, | |
child: new MaterialApp( | |
theme: new ThemeData.dark(), | |
title: appName, | |
home: new Scaffold( | |
appBar: new AppBar(title: new Text(appName)), | |
body: new StoreBuilder<AppState>( | |
onInit: (store) => store.dispatch(new ProfileRequestDataEventsAction()), | |
onDispose: (store) => store.dispatch(new ProfileCancelDataEventsAction()), | |
builder: (context, store) { | |
final profile = store.state.session.profile; | |
if (profile == null) { | |
return new Text("Loading..."); | |
} else { | |
return new Text("Hello, ${profile.firstName}"); | |
} | |
}, | |
), | |
), | |
), | |
); | |
} | |
} | |
// - Flutter - | |
// - State - | |
class AppState { | |
final String userUid; | |
final Session session; | |
AppState({ | |
this.userUid = "baus#1:)" | |
this.session = new Session.empty(), | |
}); | |
} | |
class Session { | |
final Profile profile; | |
final StreamSubscription subscription; | |
Session({ | |
this.profile, | |
this.subscription, | |
}); | |
Session.empty() | |
: profile = null, | |
subscription=null; | |
Session copyWith({Profile profile, StreamSubscription subscription}) => | |
new Session( | |
profile: profile ?? this.profile, | |
subscription: subscription ?? this.subscription, | |
); | |
} | |
class Profile { | |
final String firstName; | |
//whatevs | |
} | |
// - State - | |
// - Reducer - | |
AppState appReducer(AppState state, action) { | |
return new AppState( | |
userUid: someOtherReducer(state.userUid, action), | |
session: sessionReducer(state.session, action), | |
); | |
} | |
final sessionReducer = combineTypedReducers<Session>([ | |
new ReducerBinding<Session, ProfileDataEventsRequestedAction>(_registerSubscription), | |
new ReducerBinding<Session, ProfileOnDataAction>(_setProfile), | |
new ReducerBinding<Session, ProfileCancelDataEventsAction>(_cancelSubscription), | |
]); | |
Session _registerSubscription(Session old, ProfileDataEventsRequestedAction action) { | |
return old.copyWith(subscription: action.subscription); | |
} | |
Session _cancelSubscription(Session old, dynamic action) { | |
old.subscription?.cancel(); | |
return old.copyWith(subscription: null); | |
} | |
Session _setProfile(Session old, ProfileOnDataAction action) { | |
return old.copyWith(profile: action.profile); | |
} | |
// - Reducer | |
// - Middleware - | |
List<Middleware<AppState>> createSessionMiddleware() { | |
return combineTypedMiddleware([ | |
new MiddlewareBinding<AppState, ProfileRequestDataEventsAction>(_createProfileDataSubscription()), | |
]); | |
} | |
Middleware<AppState> _createProfileDataSubscription() { | |
return (Store<AppState> store, action, NextDispatcher next) async { | |
if (action is ProfileRequestDataEventsAction) { | |
final userUid = store.state.userUid; | |
if (userUid != null) { | |
// ignore: cancel_subscriptions | |
final sub = Firestore.instance | |
.collection("Profiles") | |
.document(userUid) | |
.snapshots | |
.listen((DocumentSnapshot doc) { | |
final profile = new Profile.fromSnapshot(doc); | |
store.dispatch(new ProfileOnDataAction(profile)); | |
}, onError: (error) => store.dispatch(new ProfileFailedLoadAction(error: error)), | |
); | |
store.dispatch(new ProfileDataEventsRequestedAction(sub)); | |
} | |
} | |
next(action); | |
}; | |
} | |
// - Middleware - | |
// - Action - | |
class ProfileRequestDataEventsAction {} | |
@immutable | |
class ProfileDataEventsRequestedAction { | |
final StreamSubscription subscription; | |
ProfileDataEventsRequestedAction(this.subscription); | |
} | |
class ProfileCancelDataEventsAction {} | |
@immutable | |
class ProfileOnDataAction { | |
final Profile profile; | |
ProfileOnDataAction(this.profile); | |
} | |
@immutable | |
class ProfileFailedLoadAction { | |
final Exception error; | |
ProfileFailedLoadAction({this.error}); | |
} | |
// - Action - |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment