Last active
January 7, 2023 18:47
-
-
Save lomza/9a44ce9431106e2160ff0cfb9e2c1da9 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
/// Environments config with GetIt | |
final getIt = GetIt.instance; | |
const dev = Environment(AppEnv.devName); | |
const prod = Environment(AppEnv.prodName); | |
@InjectableInit(preferRelativeImports: false) | |
void configureDependencies(String appEnv) => $initGetIt(getIt, environment: appEnv); | |
class AppEnv { | |
AppEnv._( | |
this.name, { | |
required this.deeplinkUri, | |
}); | |
factory AppEnv.development() => AppEnv._( | |
devName, | |
deeplinkUri: 'https://myappdev.page.link', | |
); | |
factory AppEnv.production() => AppEnv._( | |
prodName, | |
deeplinkUri: 'https://myapp.page.link', | |
); | |
static const devName = 'dev'; | |
static const prodName = 'prod'; | |
final String name; | |
final String deeplinkUri; | |
} | |
// in main_dev.dart | |
void main() => runMain(AppEnv.devName); | |
// in prod_dev.dart | |
void main() => runMain(AppEnv.prodName); | |
// in main.dart | |
configureDependencies(appEnv); | |
/// Cubit & Hooks extensions | |
T useCubit<T extends Cubit>([List<dynamic> keys = const <dynamic>[]]) { | |
// ignore: unnecessary_lambdas | |
final cubit = useMemoized(() => getIt<T>(), keys); | |
useEffect(() => cubit.close, [cubit]); | |
return cubit; | |
} | |
S useCubitBuilder<C extends Cubit, S>( | |
Cubit<S> cubit, { | |
bool Function(S current)? buildWhen, | |
}) { | |
final state = useMemoized( | |
() => cubit.stream.where(buildWhen ?? (state) => true), | |
[cubit.state], | |
); | |
return useStream(state, initialData: cubit.state).requireData!; | |
} | |
void useCubitListener<BLOC extends Cubit<S>, S>( | |
BLOC bloc, | |
BlocListener<BLOC, S> listener, { | |
bool Function(S current)? listenWhen, | |
}) { | |
final context = useContext(); | |
final listenWhenConditioner = listenWhen; | |
useMemoized( | |
() { | |
final stream = | |
bloc.stream.where(listenWhenConditioner ?? (state) => true).listen((state) => listener(bloc, state, context)); | |
return stream.cancel; | |
}, | |
[bloc], | |
); | |
} | |
typedef BlocListener<BLOC extends Cubit<S>, S> = void Function(BLOC cubit, S current, BuildContext context); | |
/// Usage in widget: | |
class BookDetailsPage extends HookWidget { | |
BookDetailsPage({ | |
Key? key, | |
this.book, | |
}) : super(key: key); | |
final Book? book; | |
final _bookFormKey = GlobalKey<FormState>(); | |
@override | |
Widget build(BuildContext context) { | |
var currentBook = book; | |
final cubit = useCubit<BookDetailsCubit>(); | |
final state = useCubitBuilder<BookDetailsCubit, BookDetailsPageState>(cubit); | |
final titleTextController = useTextEditingController(text: book?.title ?? ''); | |
final authorTextController = useTextEditingController(text: book?.author ?? ''); | |
final aboutTextController = useTextEditingController(text: book?.about ?? ''); | |
useEffect( | |
() { | |
cubit.init(book); | |
return null; | |
}, | |
[cubit], | |
); | |
useCubitListener<BookDetailsCubit, BookDetailsPageState>(cubit, (cubit, state, context) { | |
state.maybeWhen( | |
saveBook: (book) { | |
if (_bookFormKey.currentState!.validate()) { | |
Navigator.pop(context); // TODO return result back to list | |
} | |
}, | |
deleteBook: (book) { | |
if (_bookFormKey.currentState!.validate()) { | |
Navigator.pop(context); // TODO return result back to list | |
} | |
}, | |
orElse: () => null, | |
); | |
}, listenWhen: (state) => state is BookDetailsPageSaveBook); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(LocaleKeys.books_details_title_add.tr()), | |
actions: [ | |
if (book != null) | |
IconButton( | |
icon: const Icon(Icons.delete_forever_rounded), | |
tooltip: LocaleKeys.books_details_delete.tr(), | |
onPressed: () => cubit.deleteBook(book!), | |
), | |
IconButton( | |
icon: const Icon(Icons.save_rounded), | |
tooltip: LocaleKeys.books_details_save.tr(), | |
onPressed: () { | |
if (_bookFormKey.currentState!.validate()) { | |
cubit.saveBook(currentBook!); | |
} | |
}, | |
), | |
], | |
), | |
body: state.maybeWhen( | |
orElse: () => const SizedBox.shrink(), | |
loading: () => const Center(child: CircularProgressIndicator()), | |
loadBook: (book) => SingleChildScrollView( | |
child: Padding( | |
padding: const EdgeInsets.all(AppDimensions.l), | |
child: Form( | |
key: _bookFormKey, | |
autovalidateMode: AutovalidateMode.onUserInteraction, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
AppTextEdit( | |
labelText: LocaleKeys.books_details_book_title.tr(), | |
textInputType: TextInputType.name, | |
textEditingController: titleTextController, | |
validator: (value) => (value == null || value.isEmpty) ? '' : null, | |
), | |
const SizedBox(height: AppDimensions.l), | |
AppTextEdit( | |
labelText: LocaleKeys.books_details_author.tr(), | |
textEditingController: authorTextController, | |
textInputType: TextInputType.name, | |
validator: (value) => (value == null || value.isEmpty) ? '' : null, | |
), | |
const SizedBox(height: AppDimensions.l), | |
AppTextEdit( | |
labelText: LocaleKeys.books_details_about.tr(), | |
textInputType: TextInputType.multiline, | |
lines: 5, | |
textEditingController: aboutTextController, | |
validator: (value) => (value == null || value.isEmpty) ? '' : null, | |
), | |
], | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
@injectable | |
class BookDetailsCubit extends Cubit<BookDetailsPageState> { | |
final AddBookUseCase _addBookUseCase; | |
final DeleteBookUseCase _deleteBookUseCase; | |
BookDetailsCubit( | |
this._addBookUseCase, | |
this._deleteBookUseCase, | |
) : super(const BookDetailsPageState.loading()); | |
void init(Book? book) async { | |
emit(BookDetailsPageState.loadBook(....)); | |
}); | |
} | |
void saveBook(Book book) { | |
emit(BookDetailsPageState.saveBook(book)); | |
} | |
void deleteBook(Book book) { | |
emit(BookDetailsPageState.deleteBook(book)); | |
} | |
} | |
part of 'book_details_cubit.dart'; | |
@freezed | |
class BookDetailsPageState with _$BookDetailsPageState { | |
const factory BookDetailsPageState.loading() = _BookDetailsPageLoading; | |
const factory BookDetailsPageState.loadBook(Book? book) = _BookDetailsPageLoadBook; | |
const factory BookDetailsPageState.saveBook(Book book) = BookDetailsPageSaveBook; | |
const factory BookDetailsPageState.deleteBook(Book book) = _BookDetailsPageDeleteBook; | |
} | |
/// Riverpod extensions | |
import 'package:core/core.dart'; | |
import 'package:flutter/material.dart'; | |
extension StateNotifierX on ProviderListenable<AsyncValue> { | |
void listenForError(BuildContext context, WidgetRef ref, Function(Exception exception) buildErrorMessage) { | |
ref.listen<AsyncValue>(this, (previousState, currentState) { | |
currentState.whenOrNull(error: (error, _) { | |
if (error is Exception) { | |
context.showSnackBar(buildErrorMessage(error)); | |
} | |
}); | |
}); | |
} | |
void listenForData<T>(WidgetRef ref, Function(T data) onData) { | |
ref.listen<AsyncValue>(this, (previousState, currentState) { | |
currentState.whenData((data) => onData(data)); | |
}); | |
} | |
} | |
exampleNotifier.listenForData<bool>(ref, (data) { | |
if (data) { | |
context.showSnackBar('Got data!'); | |
} | |
}); | |
/// Freezed model with a custom method | |
import 'package:freezed_annotation/freezed_annotation.dart'; | |
part 'user.freezed.dart'; | |
@freezed | |
class User with _$User { | |
const User._(); | |
const factory User({ | |
required String identifier, | |
required String name, | |
List<Role>? roles, | |
}) = _User; | |
bool hasWorkRole() => roles?.any((r) => r.type == RoleType.work) ?? false; | |
} | |
/// Make use of new enums ! | |
enum RoleType { | |
home(0), | |
work(1), | |
unknown(2); | |
final int role; | |
const RoleType(this.role); | |
static RoleType toDomainModel(String role) => | |
RoleType.values.firstWhere((e) => e.number == int.parse(role), orElse: () => RoleType.unknown); | |
} | |
/// Riverpod + Freezed | |
// profile_data_state.dart | |
part 'profile_data_state.freezed.dart'; | |
@freezed | |
class ProfileDataState with _$ProfileDataState { | |
const factory ProfileDataState.loading() = Loading; | |
const factory ProfileDataState.success(User user) = Success; | |
const factory ProfileDataState.error(Exception error) = Error; | |
} | |
// profile_providers.dart | |
final getUserUseCaseProvider = Provider( | |
(ref) => GetUserUseCase(), | |
); | |
final profileDataStateProvider = | |
StateNotifierProvider.autoDispose<ProfileDataNotifier, ProfileDataState>((ref) { | |
return ProfileDataNotifier(ref.watch(getUserUseCaseProvider)); | |
}); | |
// profile_data_notifier.dart | |
class ProfileDataNotifier extends StateNotifier<ProfileDataState> { | |
final GetUserUseCase _getUserUseCase; | |
ContactFormDataNotifier(this._getUserUseCase) : super(const ProfileDataState.loading()) { | |
getProfileData(); | |
} | |
Future<void> getProfileData() async { | |
state = const ProfileDataState.loading(); | |
final result = await _getUserUseCase.execute(); | |
result.onSuccess((data) { | |
state = ProfileDataState.success(data); | |
}); | |
result.onFailure((error) { | |
state = ProfileDataState.error(error); | |
}); | |
} | |
} | |
// get_user_use_case.dart | |
class GetUserUseCase { | |
Future<Result<bool>> execute() async {...}; | |
} | |
// profile_data_screen.dart | |
class ProfileDataScreen extends ConsumerWidget { | |
@override | |
Widget build(BuildContext context) { | |
ProfileDataState state = ref.watch(profileDataStateProvider); | |
return Scaffold( | |
body: state.when( | |
loading: () => CircularProgressIndicator(), | |
success: (user) { | |
return ProfileDataForm(user); | |
}, | |
error: (reason) => ErrorWidget(), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment