Last active
July 25, 2024 08:21
-
-
Save Jerome-Jumah/25fe77e603b5a86b1182f9977965b6e2 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
import 'package:flutter_bloc/flutter_bloc.dart'; | |
import 'package:flutter/material.dart'; | |
abstract class BaseState {} | |
class InitialState implements BaseState {} | |
class LoadingState implements BaseState {} | |
class LoadedState<T> implements BaseState { | |
final T data; | |
LoadedState(this.data); | |
} | |
class ErrorState implements BaseState { | |
final String message; | |
ErrorState(this.message); | |
} | |
abstract class DataHandler<T> { | |
T transformData(dynamic rawData); | |
} | |
abstract class DataSource { | |
Future<dynamic> fetchData(); | |
} | |
class GenericViewModel<T> extends Cubit<BaseState> { | |
final DataHandler<T> dataHandler; | |
final DataSource? dataSource; | |
GenericViewModel({ | |
required this.dataHandler, | |
required this.dataSource, | |
}) : super(InitialState()); | |
Future<void> fetchData() async { | |
try { | |
emit(LoadingState()); | |
final rawData = await dataSource?.fetchData(); | |
final transformedData = dataHandler.transformData(rawData); | |
emit(LoadedState<T>(transformedData)); | |
} catch (e) { | |
emit(ErrorState(e.toString())); | |
} | |
} | |
void updateData(T newData) { | |
emit(LoadedState<T>(newData)); | |
} | |
// instead of accepting data of type T it should also accept any other data | |
void performAction<U>(T Function(T? data, [U? params]) doSomething, T data, | |
[U? params]) { | |
emit(LoadedState<T>(doSomething(data, params))); | |
} | |
} | |
class UserData { | |
final String name; | |
final int age; | |
UserData({required this.name, required this.age}); | |
} | |
class UserDataHandler implements DataHandler<UserData> { | |
@override | |
UserData transformData(dynamic rawData) { | |
return UserData( | |
name: rawData['name'], | |
age: rawData['age'], | |
); | |
} | |
} | |
class UserDataSource implements DataSource { | |
@override | |
Future<dynamic> fetchData() async { | |
// Simulating API call | |
await Future.delayed(Duration(seconds: 2)); | |
return {'name': 'John Doe', 'age': 30}; | |
} | |
} | |
class CounterHandler implements DataHandler<int> { | |
@override | |
int transformData(dynamic rawData) { | |
return rawData as int; | |
} | |
} | |
// Usage | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: MultiBlocProvider( | |
providers: [ | |
BlocProvider( | |
create: (_) => GenericViewModel<int>( | |
dataSource: null, | |
dataHandler: CounterHandler(), | |
), | |
), | |
BlocProvider( | |
create: (_) => GenericViewModel<UserData>( | |
dataHandler: UserDataHandler(), | |
dataSource: UserDataSource(), | |
)..fetchData(), | |
) | |
], | |
child: Scaffold( | |
body: UserWidget(), | |
floatingActionButton: GenericBlocConsumer<int>( | |
params: CounterConsumerArgs(), | |
), | |
), | |
), | |
); | |
} | |
} | |
class UserWidget extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Center( | |
child: GenericBlocConsumer<UserData>( | |
params: UserConsumerArgs(), | |
), | |
), | |
SizedBox(height: 4), | |
BlocBuilder<GenericViewModel<int>, BaseState>( | |
builder: (context, state) { | |
var value = state is LoadedState ? state.data : 0; | |
return ElevatedButton( | |
onPressed: () { | |
int add2(int? initialData, [int? x]) { | |
return (initialData ?? 0) + x!; | |
} | |
context | |
.read<GenericViewModel<int>>() | |
.performAction<int>(add2, value, 2); | |
}, | |
child: Text('Add two more'), | |
); | |
}), | |
], | |
); | |
} | |
} | |
class GenericBlocConsumer<T> extends StatelessWidget { | |
final CustomBlocConsumerParams params; | |
const GenericBlocConsumer({ | |
Key? key, | |
required this.params, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return BlocConsumer<GenericViewModel<T>, BaseState>( | |
listener: (context, state) { | |
params.listener(context, state); | |
}, | |
builder: (context, state) { | |
if (state is InitialState) { | |
return params.buildWhenInitial(context); | |
} else if (state is LoadingState) { | |
return params.buildWhenLoading(context); | |
} else if (state is LoadedState<T>) { | |
return params.buildWhenLoaded(context, state); | |
} else if (state is ErrorState) { | |
return params.buildWhenError(context, state.message); | |
} else { | |
return params.buildWhenOther(context, state); | |
} | |
}, | |
); | |
} | |
} | |
abstract class CustomBlocConsumerParams<T> { | |
Widget buildWhenLoaded(BuildContext ctx, LoadedState<T> state); | |
Widget buildWhenInitial(BuildContext ctx); | |
Widget buildWhenLoading(BuildContext ctx); | |
Widget buildWhenError(BuildContext ctx, String msg); | |
Widget buildWhenOther(BuildContext ctx, BaseState state); | |
listener(BuildContext ctx, BaseState state); | |
} | |
class UserConsumerArgs | |
with ConsumerDeafaultStates | |
implements CustomBlocConsumerParams<UserData> { | |
@override | |
Widget buildWhenLoaded(context, LoadedState<UserData> state) { | |
return Text('User: ${state.data.name}, Age: ${state.data.age}'); | |
} | |
@override | |
Widget buildWhenError(BuildContext ctx, String msg) { | |
return errorWidget(msg); | |
} | |
@override | |
Widget buildWhenInitial(BuildContext ctx) { | |
return initialWidget(); | |
} | |
@override | |
Widget buildWhenLoading(BuildContext ctx) { | |
return loadingWidget(); | |
} | |
@override | |
Widget buildWhenOther(BuildContext ctx, BaseState state) { | |
return unknownStateWidget(); | |
} | |
@override | |
listener(BuildContext ctx, BaseState state) {} | |
} | |
class CounterConsumerArgs | |
with ConsumerDeafaultStates | |
implements CustomBlocConsumerParams { | |
@override | |
Widget buildWhenLoaded(context, state) { | |
var val = state.data; | |
return FloatingActionButton.extended( | |
label: Text('$val'), | |
onPressed: () { | |
var value = val; | |
value += 1; | |
context.read<GenericViewModel<int>>().updateData(value); | |
}, | |
icon: Icon(Icons.add), | |
); | |
} | |
@override | |
Widget buildWhenError(BuildContext ctx, String msg) { | |
return errorWidget(msg); | |
} | |
@override | |
Widget buildWhenInitial(BuildContext ctx) { | |
var val = 0; | |
return FloatingActionButton.extended( | |
label: Text('$val'), | |
onPressed: () { | |
var value = val; | |
value += 1; | |
ctx.read<GenericViewModel<int>>().updateData(value); | |
}, | |
icon: Icon(Icons.add), | |
); | |
} | |
@override | |
Widget buildWhenLoading(BuildContext ctx) { | |
return loadingWidget(); | |
} | |
@override | |
Widget buildWhenOther(BuildContext ctx, BaseState state) { | |
return unknownStateWidget(); | |
} | |
@override | |
listener(BuildContext ctx, BaseState state) { | |
if (state is LoadedState) { | |
ScaffoldMessenger.of(ctx).showSnackBar( | |
SnackBar( | |
content: const Text('Added'), | |
), | |
); | |
} | |
} | |
} | |
mixin ConsumerDeafaultStates { | |
Widget loadingWidget() { | |
return const Center( | |
child: CircularProgressIndicator(), | |
); | |
} | |
Widget initialWidget() { | |
return const Center( | |
child: Text('Initializing...'), | |
); | |
} | |
Widget errorWidget(String message) { | |
return Center( | |
child: Text(message), | |
); | |
} | |
Widget unknownStateWidget() { | |
return Center( | |
child: Text('Unknown State'), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment