HILLEL COREN BUILDING APPS WITH FLUTTERREDUX SAMPLE Always use Built Value Avoid using a...
Transcript of HILLEL COREN BUILDING APPS WITH FLUTTERREDUX SAMPLE Always use Built Value Avoid using a...
BUILDING APPS WITH FLUTTERHILLEL COREN
A BIT ABOUT ME…
�2
APP STORE RATING
�3
“FLUTTER IS AWESOME!”
–Most developers who have tried it
�4
STATE MANAGEMENT
▸ setState
▸ Provider
▸ BLoC
▸ MobX
▸ Redux
�6
*https://blog.novoda.com/introduction-to-redux-in-flutter/
�7
REDUX PERFORMANCE
▸ Rebuilding BLoC vs Redux
▸ Importance of memoization
▸ Set distinct on the view model
▸ Keep number of layers to a minimum
�8
REDUX SAMPLE
▸ Always use Built Value
▸ Avoid using a StoreBuilder in main.dart
▸ Pass view model to the view/avoid mapping
▸ Restructure to group by feature
▸ Only need flutter_redux and redux_logging
�9
�10
�11
PERSISTENCE
▸ Separate AppState in to data, ui and auth
▸ Persist each section separately
▸ Use PersistUI and PersistData interfaces
▸ Use similar approach to track loading
▸ Clear state if app version is different
�12
abstract class AppState
implements Built<AppState, AppStateBuilder> {
bool get isLoading;
bool get isSaving;
AuthState get authState;
DataState get dataState;
UIState get uiState;
}
�13
// App actions
class StartLoading {}
class StopLoading {}
class PersistUI {}
class PersistData {}
// Client actions
class LoadClientRequest implements StartLoading {}
class LoadClientSuccess implements StopLoading,
PersistData {
final BuiltList<ClientEntity> clients;
LoadClientSuccess(this.clients);
}
�14
// App actions
class StartLoading {}
class StopLoading {}
class PersistUI {}
class PersistData {}
// Client actions
class LoadClientRequest implements StartLoading {}
class LoadClientSuccess implements StopLoading,
PersistData {
final BuiltList<ClientEntity> clients;
LoadClientSuccess(this.clients);
}
�15
// App actions
class StartLoading {}
class StopLoading {}
class PersistUI {}
class PersistData {}
// Client actions
class LoadClientRequest implements StartLoading {}
class LoadClientSuccess implements StopLoading,
PersistData {
final BuiltList<ClientEntity> clients;
LoadClientSuccess(this.clients);
}
�16
EntityUIState productReducer(ProductState state, dynamic action) {
return state.rebuild((b) => b
..editing = editingReducer(state.editing, action).toBuilder());
}
EntityUIState productReducer(ProductState state, dynamic action) {
return state.rebuild((b) => b
..editing.replace(editingReducer(state.editing, action)));
}
Error: A value of type ‘…’ can't be assigned to a variable of type ‘…Builder’.
EntityUIState productReducer(ProductState state, dynamic action) {
return state.rebuild((b) => b
..editing = editingReducer(state.editing, action).toBuilder());
}
EntityUIState productReducer(ProductState state, dynamic action) {
return state.rebuild((b) => b
..editing.replace(editingReducer(state.editing, action)));
}
Error: A value of type ‘…’ can't be assigned to a variable of type ‘…Builder’.
DATA CACHING
▸ All data is cached to the device
▸ Last updated timestamp is stored in the state
▸ Pass the timestamp when requesting new data
▸ Pull to refresh or auto-refresh after 15 minutes
▸ Autoload after certain events. ie, saving an invoice
�19
abstract class ProductState
implements Built<ProductState, ProductStateBuilder> {
int get lastUpdated;
BuiltMap<int, ProductEntity> get map;
bool get isLoaded => lastUpdated > 0;
bool get isStale {
if (! isLoaded) {
return true;
}
return DateTime.now().millisecondsSinceEpoch
- lastUpdated > kMillisecondsToRefreshData;
}
}
�20
abstract class ProductState
implements Built<ProductState, ProductStateBuilder> {
int get lastUpdated;
BuiltMap<int, ProductEntity> get map;
bool get isLoaded => lastUpdated > 0;
bool get isStale {
if (! isLoaded) {
return true;
}
return DateTime.now().millisecondsSinceEpoch
- lastUpdated > kMillisecondsToRefreshData;
}
}
�21
abstract class ProductState
implements Built<ProductState, ProductStateBuilder> {
int get lastUpdated;
BuiltMap<int, ProductEntity> get map;
bool get isLoaded => lastUpdated > 0;
bool get isStale {
if (! isLoaded) {
return true;
}
return DateTime.now().millisecondsSinceEpoch
- lastUpdated > kMillisecondsToRefreshData;
}
}
�22
VIEW MODELS
▸ View code should be UI/layout focused
▸ Nested view models
▸ Options to pass data to views
▸ Pass AppState as property on view model
▸ Pass field as property on view model
▸ Access AppState in view using context
�23
MEMOIZATION
▸ Keep build method fast
▸ Simple Dart package available: memoize
▸ memoX where X is the number of parameters
�24
List<int> activeClientsSelector(
BuiltMap<int, ClientEntity> clientMap,
BuiltList<int> clientList) {
return clientList.where((clientId) =>
clientMap[clientId].isActive).toList();
}
final memoizedActiveClients = memo2((
BuiltMap<int, ClientEntity> clientMap,
BuiltList<int> clientList) =>
activeClientsSelector(clientMap, clientList));
List<int> activeClientsSelector(
BuiltMap<int, ClientEntity> clientMap,
BuiltList<int> clientList) {
return clientList.where((clientId) =>
clientMap[clientId].isActive).toList();
}
final memoizedActiveClients = memo2((
BuiltMap<int, ClientEntity> clientMap,
BuiltList<int> clientList) =>
activeClientsSelector(clientMap, clientList));
FORMS
▸ Built Value enables change tracking
▸ Prefer didChangeDependencies() over iniState()
▸ Avoid @nullable, set default value in constructor
▸ Static negative counter for new ids
▸ Use completers in view models
�27
void _onChanged() {
final oldProduct = widget.viewModel.product;
final newProduct = oldProduct.rebuild((b) => b
..notes = _notesController.text.trim());
if (newProduct != oldProduct) {
widget.viewModel.onChanged(newProduct);
}
}
�28
void _onChanged() {
final oldProduct = widget.viewModel.product;
final newProduct = oldProduct.rebuild((b) => b
..notes = _notesController.text.trim());
if (newProduct != oldProduct) {
widget.viewModel.onChanged(newProduct);
}
}
�29
void _onChanged() {
final oldProduct = widget.viewModel.product;
final newProduct = oldProduct.rebuild((b) => b
..notes = _notesController.text.trim());
if (newProduct != oldProduct) {
widget.viewModel.onChanged(newProduct);
}
}
�30
NAVIGATION
▸ Handle navigation in middleware
▸ Pass context in actions
▸ Use completers for wrap up code
▸ Use pop() to pass back value
▸ snackbarCompleter and popCompleter
�31
Completer<Null> snackBarCompleter(BuildContext context, String message) {
final Completer<Null> completer = Completer<Null>();
completer.future.then((_) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: SnackBarRow(message: message)));
}).catchError((Object error) {
showDialog<ErrorDialog>(
context: context,
builder: (BuildContext context) => ErrorDialog(error));
});
return completer;
}
Completer<Null> snackBarCompleter(BuildContext context, String message) {
final Completer<Null> completer = Completer<Null>();
completer.future.then((_) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: SnackBarRow(message: message)));
}).catchError((Object error) {
showDialog<ErrorDialog>(
context: context,
builder: (BuildContext context) => ErrorDialog(error));
});
return completer;
}
Completer<Null> snackBarCompleter(BuildContext context, String message) {
final Completer<Null> completer = Completer<Null>();
completer.future.then((_) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: SnackBarRow(message: message)));
}).catchError((Object error) {
showDialog<ErrorDialog>(
context: context,
builder: (BuildContext context) => ErrorDialog(error));
});
return completer;
}
switch (action) {
case EntityAction.archive:
store.dispatch(ArchiveInvoiceRequest(
snackBarCompleter(context, localization.archivedInvoice),
invoice.id));
break;
...
}
CODE GENERATOR
▸ Reduces need for a lot of copy/replace
▸ Two main commands:
▸ Init: Update code with your app info
▸ Make: Generate individual module
▸ Creates a large amount of code
▸ Important to get it right at the beginning
�36
PACKAGES
▸ http
▸ path_provider
▸ shared_preferences
▸ font_awesome_flutter
▸ built_value/built_collection
▸ memoize
▸ url_launcher
�37
▸ flutter_slidable
▸ charts_flutter
▸ google_sign_in
▸ firebase_auth
▸ local_auth
▸ share
▸ cached_network_image
BEST PRACTICES
▸ analysis_options.yaml
▸ sentry.io or Crashlytics
▸ Built Value
▸ Create lots of widgets, ie. IconText
▸ Read the flutter_redux code
�38
APP STORE RATING
�39
APP STORE RATING
�40
PLATFORMS: CURRENT
�41
Mobile
PLATFORMS: NEXT
�42
Mobile
Desktop
PLATFORMS: FUTURE
�43
Mobile
Desktop
Web
WHO TO FOLLOW
▸ Tim Sneath - @timsneath
▸ Eric Seidel - @_eseidel
▸ Martin Aguinis - @MartinAguinis
▸ Nilay Yener - @nlycskn
▸ Andrew Brogdon - @RedBrogdon
▸ Emily Fortuna - @bouncingsheep
▸ Filip Hráček - @filiphracek
�44
IT’S ALL WIDGETS!
�45
HILLEL.DEV@HILLELCOREN