Created
August 18, 2021 23:04
-
-
Save ricbermo/046b38d1c70927be9ca78654a0c237ba to your computer and use it in GitHub Desktop.
Repository-Provider-Controller Patter
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
// custom provider to improve lists performance | |
final currentModel = ScopedProvider<Category>( | |
(_) => throw UnimplementedError(), | |
); | |
// used to display snackbars | |
final exceptionProvider = StateProvider<CustomException>( | |
(_) => null, | |
); | |
final modelListController = StateNotifierProvider<ListController>((ref) => ListController(ref.read)..retrieveData()); | |
class ListController extends StateNotifier<AsyncValue<List<Model>>> { | |
final Reader _read; | |
ListController(this._read) : super(const AsyncValue.loading()); | |
Future<void> retrieveData() async { | |
try { | |
state = const AsyncValue.loading(); | |
final items = await _read( | |
repositoryProvider, | |
).retrieveData(); | |
if (mounted) { | |
state = AsyncValue.data(items); | |
} | |
} on CustomException catch (e) { | |
_read(exceptionProvider).state = e; | |
} | |
} | |
} |
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
class CustomException implements Exception { | |
final String message; | |
const CustomException({this.message = 'Something went wrong!'}); | |
@override | |
String toString() => 'CustomException { message: $message }'; | |
} |
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
import 'package:dio/dio.dart'; | |
import 'package:hooks_riverpod/hooks_riverpod.dart'; | |
import 'package:shared_preferences/shared_preferences.dart'; | |
final dioProvider = Provider<Dio>((ref) { | |
final dio = Dio(); | |
dio.options.baseUrl = "path/to/server"; | |
return dio; | |
}); | |
// to understand this, please refer to | |
// https://www.youtube.com/watch?v=J2iFYZUabVM&t=1041s | |
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) { | |
throw UnimplementedError(); | |
}); |
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
class GridView extends HookWidget { | |
@override | |
Widget build(BuildContext context) { | |
final controller = useProvider(modelListController.state); | |
return controller.when( | |
data: (items) => Wrap( | |
runSpacing: 16, | |
alignment: WrapAlignment.spaceBetween, | |
children: List.generate(items.length, (index) { | |
final item = items[index]; | |
return ProviderScope( | |
overrides: [currentModel.overrideWithValue(item)], | |
child: const ModelTile(), | |
); | |
}), | |
), | |
loading: () => const SkeletonLoader(), | |
error: (error, _) { | |
return Text( | |
error is CustomException ? error.message : 'Something went wrong!'); | |
}, | |
); | |
} | |
} |
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
final repositoryProvider = Provider<ModelBaseRepository>( | |
(ref) => ModelRepository(ref.read), | |
); | |
class ModelRepository implements ModelBaseRepository { | |
final Reader _read; | |
const ModelRepository(this._read); | |
@override | |
Future<List<Model>> retrieveData() async { | |
try { | |
final response = await _read(dioProvider).get( | |
'/path', | |
); | |
final data = Map<String, dynamic>.from( | |
response.data, | |
); | |
final jsonAPI = Japx.decode(data); | |
final results = List<Map<String, dynamic>>.from(jsonAPI['data']); | |
final List<Model> listOfItems = results | |
.map( | |
(data) => Model.fromMap(data), | |
) | |
.toList(growable: false); | |
return listOfItems; | |
} on DioError catch (err) { | |
final message = err.response?.statusMessage ?? 'Something went wrong!'; | |
throw CustomException(message: message); | |
} on SocketException catch (_) { | |
const message = 'Please check your connection.'; | |
throw const CustomException(message: message); | |
} | |
} | |
} |
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
import 'package:mind_peace/models/a_random_model.dart'; | |
abstract class ModelBaseRepository { | |
Future<List<Model>> retrieveData(); | |
} |
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
//use ProviderListener to display the snackbar | |
body: ProviderListener( | |
provider: exceptionProvider, | |
onChange: ( | |
BuildContext ctx, | |
StateController<CustomException> exception, | |
) { | |
Scaffold.of(ctx).showSnackBar( | |
SnackBar( | |
content: Text(exception.state?.message), | |
), | |
); | |
}, | |
child: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Container(), | |
), | |
), |
@ewilliams-zoot AsyncValue.data([ ...state.data?.value, newItem ])
looks good but I'm using Freezed to facilitate things, like this
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'models/note.dart';
part 'notes_state.freezed.dart';
@freezed
abstract class NotesState implements _$NotesState {
const factory NotesState({
@Default(1) int page,
@Default(true) bool isLoading,
List<Note> notes,
}) = _NotesState;
const NotesState._();
factory NotesState.init() => const NotesState(
page: 1,
notes: [],
);
}
//controller
class NotesController extends StateNotifier<NotesState> {
final Reader _read;
NotesController(
this._read, [
NotesState state,
]) : super(state ?? NotesState.init());
Future<void> fetch({bool refreshing = false}) async {
try {
final notes = await _read(
journalNotesRepositoryProvider,
).fetch();
if (mounted) {
state = state.copyWith(
notes: refreshing ? notes : [...state.notes, ...notes],
isLoading: false,
);
}
} on CustomException catch (e) {
_read(journalNotesListExceptionProvider).state = e;
state = state.copyWith(isLoading: false);
}
}
Future<void> _createNote(String title, String content) async {
try {
final note = await _read(
journalNotesRepositoryProvider,
).saveNote(title: title, content: content);
if (mounted) {
state = state.copyWith(
notes: [note, ...state.notes],
);
_read(currentEditingNote).state = note;
_read(journalNotesSuccessProvider).state = 'Saved'; //snackbar trick
}
} on CustomException catch (e) {
_read(journalNotesListExceptionProvider).state = e;
}
}
}```
The rest of CRUD actions will be the same :sweat_smile:. You would just need to change the params, the request method and how you are overriding the state.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ricbermo , if we were to call an endpoint that would add an item to our list, how would the list controller copy the original list and add the new item to it with the AsyncData/AsyncValue being wrapped around it? My naive attempt looks something like
Or, would you cache the list as a property of the repository class?
Or would you just re-fetch the whole list any time there's an interaction with the server?