Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major codebase cleanup #415

Merged
merged 32 commits into from
Jan 7, 2025
Merged

Major codebase cleanup #415

merged 32 commits into from
Jan 7, 2025

Conversation

mocodesmo
Copy link
Collaborator

INFO: further clean up, rearranging and renaming will be done after this PR is merged

Removed all/most bloc-to-bloc communications

  • removed passing walletblocs via routes
  • removed walletbloc property in blocstate
  • removed passing currencycubit into blocs
  • removed passing networkcubit into blocs

WALLET SERVICE

  • all wallet operations now handled by walletservice
  • exposes a stream of walletdata which is listened to by walletbloc

APP WALLET REPOSITORY

  • main repository that maintains all walletservices (which contains each wallet)
  • handles loading wallets from storage
  • handles getters for wallet/walletservice retrieval

NETWORK REPOSITORY

  • handles network connections and updates
  • exposes a stream of networkrepodata which is listened to by networkkbloc

APP WALLETS BLOC

  • created in locator on appstart
  • handles the list of walletblocs which is used for retrieval across the app

Copy link
Contributor

@kumulynja kumulynja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some general remarks:

  • Good first chunk of refactoring to remove some Cubit in Cubit’s already and start with repository classes.

  • But I think the scope of the current repositories is still too big and should be limited as much as possible to just storing and retrieving data, for example the NetworkRepository now also creates a blockchain object for the wallet. I think a cleaner flow would be: NetworkRepository is only responsible for storing and retrieving the settings concerning the network, then a WalletRepository or WalletService is responsible for creating the blockchain instance it needs. Both the NetworkRepository as the WalletRepository/Service can be injected into the BloC or Service that needs to initialise the wallets. In that BLoC, the NetworkRepository can then be used to retrieve the current network settings from local storage and these can then be passed to the WalletRepository/Service to initialise it with those correct settings. This way they are both decoupled from each other and the BLoCs or Services can use both. Now having the creation of the bdk Blockchain instance in the NetworkRepository is too tightly coupled code. In general I think we should improve by keeping the repositories as limited as possible to just storing and retrieving data. Initializing things or using this data should move to higher layers like Services for global reusable things or to BLoCs for feature specific logic.

  • If the scope of repositories is more bounded, we can organise the architecture a lot better in different layers and make sure there is a clear pattern and consistency in responsibilities of every layer, what is injected into what and how data and calls flow between them. For example, normally repositories get injected to services so that the services can build functionality that needs data from different repositories, but the AppWalletsRepository keeps a list of WalletServices, and the repository creates the services, that’s kind of the other way around. If we keep a more consistent pattern of layers and how injections, data and calls flow through them, the code will become more understandable and easier to extend on.

  • Another practice I think we should apply more in the future is the use of abstract classes/interfaces. This to also create less coupled and more reusable code and try to keep external dependencies like bdk limited to only a secure BitcoinWalletService class, while implementing a general WalletService class for all wallet types. Using abstract classes and then implementing concrete types can also eliminate the need to check for isLiquid, isBitcoin etc, which is repeated a lot in the code.

  • I think we can still improve a lot in how the BLoC pattern is applied:

    • The list of WalletBloc's in the AppWalletBlocs cubit state is still a Bloc in another Bloc, which is an anti-pattern and should also be able to be done differently.
    • States still do not really have different states in the sense of having a Status enum with the different states this BloC can be in (e.g. inital, loading, success, failure) or having a sealed class for shared properties and subclasses for every distinct state/step a feature or BLoC can be in. Without this, the BLoC pattern can not really be used as effective and more code and if/else’s are needed to figure out what state the BloC is really in now which leads to less maintainable and understandable code.
    • The CurrencyCubit still holds state like amounts that logically should be part of other BloCs like the amount to send should be managed by the SendBloc and the amount to receive should be managed by the ReceiveBloc. The CurrencyCubit’s state should be limited to the available and selected currencies and the exchange rate in my opinion, they should not manage state of different features. I think this is a general problem that various Cubits are defined on a global level, while their State and its values are logically more part of other BLoC’s of other features.

I also think that to continue this BLoC refactoring we can do it more gradually bloc per bloc, starting with the easiest maybe like the NetworkBloc, then the CurrencyCubit and everything involved with that (currency repository, Currency cubit state, separating the amount from it and adding it to the send and receive state itself etc.), then the rest of the ReceiveCubit, then the rest of the SendCubit etc…
This PR was pretty big to review all at once and pretty difficult to really understand all implications and changes.

@@ -515,6 +516,10 @@ class Wallet with _$Wallet {
List<SwapTx> swapsToProcess() {
return swaps.where((swap) => swap.proceesTx() && !swap.failed()).toList();
}

int balanceSats() => balance ?? 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not change the balance field/parameter to balanceSats directly with a default value of 0 instead of having balance which is unclear if it is in sats or btc or fiat and then having a function for balanceSats which just returns balance or 0… I think this can be clearer by just replacing the balance field for balanceSats directly with a default 0 value.


int balanceSats() => balance ?? 0;

String balanceStr() => ((balance ?? 0) / 100000000).toStringAsFixed(8);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The balanceStr seems to be the balance in btc and as a String then, but the fact that it is in btc is not clear from the name. balanceBtc would be a better name I think, and I would leave it as a numeric value (preferably Decimal type) and leave String formatting up to the presentation/UI layer and not the model. The intl package number formatting capabilities (NumberFormat.currency) can be used in the Widgets for a formatted String of a currency balance like this.

@@ -39,8 +40,8 @@ class DeepLink {
Future<Err?> handleUri({
required String link,
// required SettingsCubit settingsCubit,
required HomeCubit homeCubit,
required NetworkCubit networkCubit,
required HomeBloc homeCubit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HomeBloc is renamed, but the variable is still named homeCubit.
Same for networkCubit.

@@ -13,6 +13,7 @@ class StorageKeys {
static const wallets = 'wallets';
static const settings = 'settings';
static const network = 'network';
static const networkReposity = 'networkReposity';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear why a repository itself should have a key to be stored itself...?


final List<Wallet> walletsWithEnoughBalance = [];

for (final walletBloc in wallets) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename walletBloc to wallet since it is not a Bloc anymore.


class WalletService {
WalletService({
required Wallet wallet,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be clearer if a WalletService creates a Wallet instance itself in its constructor or some create or init function, instead of already getting one injected. Definitely if it also gets the WalletStorageRepository passed, which is good. Having that repository should be what the service needs to retrieve the Wallet instead of getting injected both.

Comment on lines +42 to +46
required WalletSync walletSync,
required WalletBalance walletBalance,
required WalletAddress walletAddress,
required WalletCreate walletCreate,
required WalletTx walletTransaction,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having the WalletCreate, WalletSync and WalletAddress as “global” instances through locator being passed into the service, wouldn't it be clearer if the WalletService itself is a Global instance and it just has the functions from WalletSync etc in it instead of getting them injected in? It could create and keep a Wallet instance in memory instead of having to pass it to everyone of these classes.

import 'package:bb_mobile/address/bloc/address_state.dart';
import 'package:bb_mobile/wallet/bloc/event.dart';
import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class AddressCubit extends Cubit<AddressState> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this Cubit can be separated and replaced with repositories and wallet services to make things more straight forward.

@@ -35,6 +37,12 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';

class AppWalletBlocs extends Cubit<List<WalletBloc>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a WalletBloc inside another Cubit, this is really an anti-pattern.
Why not have methods in the HomeBloc itself to clearWallets or update the Wallet’s that are available? If this is possible to do from the home page by the user, this should just be part of the HomeBloc I think.


for (final bloc in blocs) {
context
.read<HomeLoadingCubit>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a HomeLoadingCubit , shouldn’t we just have a loading state in the HomeBloc state itself?
If we pass the wallet repositories to the HomeBloc, the Bloc itself can listen to events of it and change its state from and to Loading.

@ethicnology ethicnology changed the base branch from main to refactoring-bloc January 7, 2025 16:09
@ethicnology ethicnology merged commit 5140ece into refactoring-bloc Jan 7, 2025
1 of 3 checks passed
ethicnology pushed a commit that referenced this pull request Jan 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants