mardi 5 décembre 2023

Dynamic BLoC Implementation Switching in Flutter Using Bloc Pattern and Custom Class Loader

I'm facing a challenge in designing a Flutter application with the BLoC state management pattern. The app is intended to be generic but used by multiple clients, some of whom have specific needs requiring customized versions of the main app.

To achieve this, I've implemented a custom class loader that dynamically loads either the default class or an overridden specific class. This applies not only to the app's main classes but also to BLoC implementations. For example, I have a StockBloc and an overridden version called SpeStockBloc. The registration of overridden classes happens at the beginning of the app using MyClassLoader()[StockBloc] = SpeStockBloc.new.

The issue arises when I attempt to consume the StockBloc in a view using context.watch<StockBloc>. Instead of recognizing the overridden SpeStockBloc, it looks for the exact StockBloc type in the widget tree. I understand that it's the correct behavior of Bloc and Provider.

I'm seeking advice or alternative approaches on how to dynamically switch between BLoC implementations at runtime while utilizing context.watch or a similar mechanism, ensuring it takes overridden types into account.

Any insights or suggestions on how to handle this scenario efficiently would be greatly appreciated.

Custom class loader :

/// Class to provide (if registered) a construction function that overrides
/// the initial construction for the [Type] used as a parameter
class DynamicClassLoader {
  final Map<Type, Function> _mappedClasses = <Type, Function>{};

  static final DynamicClassLoader _instance = DynamicClassLoader._internal();

  /// Singleton factory
  factory DynamicClassLoader() {
    return _instance;
  }

  DynamicClassLoader._internal();

  /// Checks if a specific class is registered
  bool hasOverride(Type type) =>
      _mappedClasses.containsKey(type) && _mappedClasses[type] != null;

  /// Access operator for a construction function of an instance of type [type]
  Function? operator [](Type type) {
    return _mappedClasses[type];
  }

  /// Obtains an instantiation function for the overridden class
  Function newInstantiation(Type type, Function fallback) =>
      hasOverride(type) ? this[type]! : fallback;

  /// Assignment operator for a construction method for a type [type]
  void operator []=(Type type, Function func) {
    _mappedClasses.update(
      type,
      (
        Function value,
      ) =>
          func,
      ifAbsent: () => func,
    );
  }
}

/// Access function to the method [DynamicClassLoader.newInstantiation]
Function newInstantiation<Class>(Function fallback) =>
    _newInstantiation(Class, fallback);

Function _newInstantiation(Type type, Function fallback) =>
    DynamicClassLoader().newInstantiation(type, fallback);

Stock bloc :

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
import 'package:test_bloc/class_loader.dart';

class StockBloc extends Bloc<StockEvent, StockState> {
  /// factory
  factory StockBloc({required int myCustomParam}) =>
      newInstantiation<StockBloc>(StockBloc.std)(myCustomParam: myCustomParam);

  StockBloc.std() : super(StockInitial()) {
    on<StockEvent>((event, emit) {
      // TODO: implement event handler
    });
  }
}

@immutable
class StockEvent {}

@immutable
sealed class StockState {}

final class StockInitial extends StockState {}

spe stock bloc :

import '../stock_bloc.dart';

class SpeStockBloc extends StockBloc {
  SpeStockBloc() : super.std() {
    on<StockEvent>((event, emit) {
      // TODO: implement event handler
    });
  }
}

sub_cmp :

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:test_bloc/bloc/bloc/stock_bloc.dart';

class SubCmp extends StatelessWidget {
  const SubCmp({super.key});

  @override
  Widget build(BuildContext context) {
    StockState bloc = context.watch<StockBloc>().state;

    return const Text("Sub cmp");
  }
}

main :

import 'package:flutter/material.dart';
import 'package:test_bloc/bloc/bloc/bloc/spe_stock_bloc.dart';
import 'package:test_bloc/bloc/bloc/stock_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:test_bloc/class_loader.dart';
import 'package:test_bloc/sub_cmp.dart';

void main() {
  runApp(
    const MyApp(),
  );
}

void registerSpe() {
  DynamicClassLoader()[StockBloc] = SpeStockBloc.new;
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: BlocProvider(
        create: (context) => StockBloc(myCustomParam: 10),
        child: const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              SubCmp(),
            ],
          ),
        ),
      ),
    );
  }
}


Aucun commentaire:

Enregistrer un commentaire