Skip to content

Instantly share code, notes, and snippets.

@ricbermo
Created August 18, 2021 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ricbermo/046b38d1c70927be9ca78654a0c237ba to your computer and use it in GitHub Desktop.
Save ricbermo/046b38d1c70927be9ca78654a0c237ba to your computer and use it in GitHub Desktop.
Repository-Provider-Controller Patter
// 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;
}
}
}
class CustomException implements Exception {
final String message;
const CustomException({this.message = 'Something went wrong!'});
@override
String toString() => 'CustomException { message: $message }';
}
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();
});
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!');
},
);
}
}
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);
}
}
}
import 'package:mind_peace/models/a_random_model.dart';
abstract class ModelBaseRepository {
Future<List<Model>> retrieveData();
}
//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
Copy link

@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

state = AsyncValue.data([ ...state.data?.value, newItem ]);

Or, would you cache the list as a property of the repository class?

// in repository
final Reader _read;
final List<Model> currentList = [];

@override
Future<List<Model>> retrieveData() async { /* currentList = responseMap['items'] */ }

// ...

// in controller
Future<void> startChallenge() {
  final repository = _read(repository);
  // modify database state server-side
  final result = await repository.startChallenge();

  // if result is good, modify local UI data without fetching the whole list
  final cachedList = repository.currentList;
  state = AsyncValue,data([ ...cachedList, newItem ]);
}

Or would you just re-fetch the whole list any time there's an interaction with the server?

@ricbermo
Copy link
Author

@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