Skip to content

Instantly share code, notes, and snippets.

@ricbermo
Created August 18, 2021 23:04
Show Gist options
  • 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(),
),
),
@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