potatoes

Creator: coderz1093

Last updated:

0 purchases

potatoes Image
potatoes Images
Add to Cart

Description:

potatoes

Potatoes #

Potatoes or french fries?
Potatoes is a lightweight development kit based on BLoC,
providing handy classes to help you quickly build your apps.
Index #

Potatoes
Index

Overview


Potatoes cubits
Cubit states

ObjectCubit
CubitManager
ValueCubit


Services

ApiService & Dio client
PreferencesService


Loader widgets

AutoListView
AutoContentView


Loader dialogs

CompletableMixin
Dialogs


Phoenix
Library imports

Overview #
This package is meant to ease the writing of repetitive logics in code by providing
a set of classes and tools based on BLoC pattern. If you are not familiar with BLoC,
please head first here.
Potatoes also provide a simply state logic to help you loop effectively through your
cubits states.
Potatoes cubits #
Cubit states #
In Potatoes logic, cubit states are steps of a cycle alternating from one to another.
A cubit state can be permanent and providing an user interface or punctual just to
notify some listener. You can attribute a role to a state by extending the
corresponding class:
// First declare the state base class.
// Cubit states classes extend Equatable
mixin LoginState on Equatable {}

// Use CubitSuccessState for idle or permanent success states. Equatable.props defines
// when the cubit state should be refreshed based on objects equality.
class LoginIdleState extends CubitSuccessState with LoginState {
final String? email;
final String? password;

const LoginIdleState.empty() : email = null, password = null;

const LoginIdleState(this.email, this.password);

@override
List<Object?> get props => [email, password];
}

// Use CubitLoadingState for loading steps. They cast a loading screen and prevent
// the user from running a request twice.
class LoggingInState extends CubitLoadingState with LoginState {
const LoggingInState();
}

// CubitInformationState are one-off events that dispatch a specific information
// without breaking the execution of a sequence.
// Use this to show a dialog or fire a one-time action.
class LoginNeedsOTPState extends CubitInformationState with LoginState {
const LoginNeedsOTPState();
}

// CubitErrorState are used to track errors within an a sequence inside a cubit.
// Providing the error cause and the stack trace allow them to be logged via CubitErrorState.stream
class LoginErrorState extends CubitErrorState with LoginState {
LoginErrorState(super.error, [super.trace]);
}
copied to clipboard
Each cubit state can be associated to one of these roles and while they do not directly
impact your state classes, they provide you some sort of logic to follow when writing your
cubit methods.
Here is an example with a login cubit:
class LoginCubit extends Cubit<LoginState> {
final AuthService authService;

LoginCubit(this.authService) : super(const LoginIdleState.empty());

void login() {
// fires action only if current state is idle
if (state is LoginIdleState) {
final stateBefore = state as LoginIdleState;
// cast loading behavior
emit(const LoggingInState());
authService.login(
stateBefore.email,
stateBefore.password
).then(
(response) {
if (response.shouldValidateOTP) {
// go to OTP page
emit(const LoginNeedsOTPState());
// revert to success state
emit(stateBefore);
} else {
// direct login
// information state
emit(const LoggedInState());
// revert to empty success state
emit(const LoginIdleState.empty());
}
},
onError: (error, trace) {
// log error and go back to last success state
emit(LoginErrorState(error, trace));
emit(stateBefore);
}
);
}
}
}
copied to clipboard
As you can see, by naming each state purpose, performing a cubit action becomes quite
as easy as to state it in natural language.
Depending on the case, you should arrange state roles within a cubit method execution.
For example, a data fetcher cubit should start with a CubitLoadingState and may
consider a CubitErrorState as a durable state.
ObjectCubit #
An ObjectCubit is an implementation of a Cubit designed to handle the lifecycle
of a single object. This is specifically effective to track business objects as
you might want to update them while still tracking the same Cubit.
ObjectCubit ensures that you can get the last version registered version
of the tracked object at anytime, regardless of the current cubit state.
class PostCubit extends ObjectCubit<Post, APostState> {
PostCubit(Post post) : super(PostState(post));

// each time a new state is emitted, this method is called to update [this.object].
// Relying on [this.object] allows us to access to the latest version of the
// tracked object without depending on the current state.
@override
Post? getObject(APostState state) {
if (state is PostState) {
return state.post;
} else {
return null;
}
}

// this method defines the object update logic, based on external events.
// [ObjectCubit.update] is called by external source, providing a possibly updated version
// of the tracked data. Decide here if the cubit should be updated.
@override
void update(Post object) {
if (this.object == null) {
emit(PostState(object));
} else if (object.lastUpdate > this.object!.lastUpdate) {
emit(PostState(object));
}
}
}
copied to clipboard
CubitManager #
A CubitManager is a factory for a single Cubit type. It handles the
lifecycle of the cubits of a specific type by assigning id to each instance.
This is handy when you want to ensure that only one cubit of each id is
used across your app.
A classic use case could be managing posts cubits inside and app, with posts
objects that can be edited as the app runs. CubitManager will ensure that
only one cubit is associated to a specific post (given a unique post ID).
While using CubitManager, you may not want the cubits to be automatically
closed by widgets such as BlocProvider. Be sure to always use
BlocProvider.value instead of the default constructor, as the latest
internally handle the created cubit lifecycle.
class PostCubitManager extends CubitManager<PostCubit, Post, int> {
// build the unique identifier of an object, used to track the uniqueness of each cubit
@override
int buildId(Post object) {
return object.id;
}

// instantiate a new cubit based on a business object
@override
PostCubit create(Post object) {
return PostCubit(object);
}

// update a currently existing cubit with an updated version of the same tracked object
@override
void updateCubit(PostCubit cubit, Post object) {
cubit.update(object);
}
}
copied to clipboard
ValueCubit #
A miscellaneous cubit used to track simple class values.
final counterCubit = ValueCubit<int>(0);
counterCubit.set(1);

final counterResetCubit = InitialValueCubit<int>(0);
counterResetCubit.set(1);
counterResetCubit.reset(); // go back to initial value
copied to clipboard
Services #
ApiService & Dio client #
ApiService is an abstract class representing an API repository. It comes with these
simplifications:

dynamic base url based on Links.server
requests execution logging
simplified authorization headers injection
responses custom parsing
unified API error class

Creating an ApiService #
To set up ApiServices in your project, you should first define Links url values.
class Links extends potatoes.Links {
const Links();

@override
String get devUrl => 'development url here';

@override
String get productionUrl => 'staging/pre-prod url here';

@override
String get stagingUrl => 'production url here';

/// other handy links
}
copied to clipboard
One of these links is selected to initialize the Dio instance when using DioClient.instance.
You still can provide an unrelated url to DioClient by using the baseUrl parameter.
final dio = potatoes.DioClient.instance(
preferencesService,
baseUrl: 'custom url to override Links.server',
connectTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 50),
receiveTimeout: const Duration(minutes: 5),
// whether all http status code should lead to a future success
disableStatusesErrors: false
);
copied to clipboard
Keep in mind that you still can set Options values by calling dio.options.
Finally, setting up the base ApiService for the project is done by overriding the
compute method:
class CustomApiService extends ApiService {
const CustomApiService(super._dio);

@override
Future<T> compute<T>(
// basically the dio.fetch call
Future<Response<dynamic>> request, {
// a key to look for, where to-parse data are located
String? mapperKey,
// an object mapper
T Function(Map<String, dynamic> p1)? mapper,
// a string mapper
T Function(String p1)? messageMapper
}) async {
try {
final response = await request;

// ... compute response

// ... use mapper or messageMapper to return result
} on DioException catch (error) {
throw ApiError.fromDio(error);
} catch (error, trace) {
throw ApiError.unknown(error.toString(), trace);
}
}
}
copied to clipboard
compute method is the general way your app will parse data coming from the queried
remote API. If you have no idea on how to write your compute method, have a look at
ApiService.defaultExtractResult as an example.
Using an ApiService #
To get advantage of the capabilities of ApiService, you should extends your custom
class and begin creating concrete methods.
class AuthService extends CustomApiService {
const AuthService(super._dio);

Future<LoginResponse> login({
required String email,
required String password
}) {
// compute will run the POST query and parse the result accordingly to the
// implementation in CustomApiService.
// Based on parameters, it will look for 'data' key in the response json and then
// parse its value using LoginResponse.fromJson constructor
return compute(
dio.post(
'/login',
data: {
'email': email,
'password': password
}
),
mapperKey: 'data',
mapper: LoginResponse.fromJson
);
}
}
copied to clipboard
PreferencesService #
PreferencesService is a wrapper of SharedPreferences
designed to offer a better preferences management and providing new capabilities.
Creating a PreferencesService #
You can create your custom PreferencesService by extending this class.
class AppPreferencesService extends PreferencesService {
static const String _tokenKey = 'token';

AppPreferencesService(super.preferences);

Future<void> saveToken(String value) => preferences.setString(_tokenKey, value);

@override
FutureOr<Map<String, String>> getAuthHeaders() {
/// set headers setup logic here to be used by DioClient
return {
'Authorization': preferences.getString(_tokenKey)!
};
}
}
copied to clipboard
The preferred logic is to set all your preferences entry keys as const values and use them as above.
getAuthHeaders is a method called by DioClient each time you set a withAuth() while doing a Dio request.
Future<void> someApiCall() {
return compute(
dio.post(
'/route',
/// this will inject the result of `getAuthHeaders` before executing this request
options: Options(headers: withAuth())
)
);
}
copied to clipboard
Secure Preferences management #
If you want to store your preferences into a secure storage, you can use the addon
Potatoes Secured Preferences.
Loader widgets #
AutoListView #
AutoListView displays a list of paginated items that updates automatically. It handles
an empty builder, loadingBuilder, loadingMoreBuilder and errorBuilder. The item list is
obtained using AutoListCubit.
AutoListView.get<Post>(
cubit: AutoListCubit(
provider: ({int page = 0}) => postService.getPosts(page: page)
),
itemBuilder: (context, post) => PostItem(post: post),
emptyBuilder: (context) => const Center(
child: Text("Empty list"),
),
errorBuilder: (context, retry) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("An error occured"),
TextButton(
onPressed: retry,
child: const Text("Retry"),
)
],
)
)
copied to clipboard
When using an AutoListCubit with custom states, use defaultBuilder to
provide appropriate display per state.
AutoContentView #
AutoContentView acts as same as AutoListView but provides a single item
at the end. It uses AutoContentCubit to obtain the data and manage its state.
AutoContentView.get<User>(
cubit: AutoContentCubit(
provider: userService.getUser(id: user.id),
),
builder: (context, user) => UserDisplay(user: user),
errorBuilder: (context, retry) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("An error occured"),
TextButton(
onPressed: retry,
child: const Text("Retry"),
)
],
)
)
copied to clipboard
Loader Dialogs #
CompletableMixin #
CompletableMixin is a State extension that allows you to seamlessly add a whole
screen loader to your screen. Use loadingDialogCompleter to track the BuilContext
used to display your loader, and call waitForDialog to dismiss it. This is
typically used in conjunction with showLoadingBarrier or showSimpleLoadingBarrier.
class MyScreen extends StatefulWidget {
const MyScreen({super.key});

@override
State<MyScreen> createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> with CompletableMixin {
@override
Widget build(BuildContext context) {
return const Placeholder();
}

// example listener for a BlocListener
void onEventReceived(BuildContext context, CustomState state) async {
// if the loader is currently displayed, removes it
await waitForDialog();

if (state is LoadingState) {
// display the loader and pass its completer to loadinDialogCompleter
loadingDialogCompleter = showLoadingBarrier(context: context);
}
}
}
copied to clipboard
Dialogs #
Potatoes provides two built-in loaders:
Dialog popup loader #
loadingDialogCompleter = showLoadingBarrier(context: context, text: "Please wait...");
copied to clipboard
Barrier-only loader #
loadingDialogCompleter = showSimpleLoadingBarrier(context: context);
copied to clipboard
Phoenix #
Phoenix allows you to entirely rebuild your app from a call using the current context.
void main() {
runApp(
const Phoenix(
child: MyApp()
)
);
}
copied to clipboard
Then:
// rebuild whole app
Phoenix.rebirth(context);
copied to clipboard
Library imports #
Potatoes depends on the following packages:

Dio
Equatable
Flutter BLoC
Shared Preferences

You may want to access to these package classes without having to extra-importing them.
In such case, use the following import:
import 'package:potatoes/libs.dart';
copied to clipboard

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files In This Product:

Customer Reviews

There are no reviews.