From c72339a413dba295ddfa692e401657918df5b390 Mon Sep 17 00:00:00 2001 From: vjrj <vjrj@comunes.org> Date: Wed, 3 May 2023 01:46:41 +0200 Subject: [PATCH] Refactor and Transactions page bloc --- .../models}/bottom_nav_cubit.dart | 0 lib/data/models/pending_transaction.dart | 33 +++++ lib/data/models/pending_transaction.g.dart | 123 ++++++++++++++++++ lib/{cubit => data/models}/theme_cubit.dart | 0 lib/{cubit => data/models}/theme_state.dart | 0 lib/data/models/transactions_bloc.dart | 103 +++++++++++++++ lib/data/models/transactions_state.dart | 13 ++ lib/ui/screens/fifth_screen.dart | 4 +- lib/ui/screens/first_screen.dart | 2 +- lib/ui/screens/fourth_screen.dart | 2 +- lib/ui/screens/second_screen.dart | 2 +- lib/ui/screens/skeleton_screen.dart | 2 +- lib/ui/screens/third_screen.dart | 8 +- lib/ui/widgets/bottom_nav_bar.dart | 2 +- lib/ui/widgets/first_screen/pay_form.dart | 4 +- lib/ui/widgets/first_screen/theme_card.dart | 2 +- .../fourth_screen/transaction_page.dart | 33 ++++- .../widgets/third_screen/contacts_page.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 20 files changed, 315 insertions(+), 23 deletions(-) rename lib/{cubit => data/models}/bottom_nav_cubit.dart (100%) create mode 100644 lib/data/models/pending_transaction.dart create mode 100644 lib/data/models/pending_transaction.g.dart rename lib/{cubit => data/models}/theme_cubit.dart (100%) rename lib/{cubit => data/models}/theme_state.dart (100%) create mode 100644 lib/data/models/transactions_bloc.dart create mode 100644 lib/data/models/transactions_state.dart diff --git a/lib/cubit/bottom_nav_cubit.dart b/lib/data/models/bottom_nav_cubit.dart similarity index 100% rename from lib/cubit/bottom_nav_cubit.dart rename to lib/data/models/bottom_nav_cubit.dart diff --git a/lib/data/models/pending_transaction.dart b/lib/data/models/pending_transaction.dart new file mode 100644 index 00000000..648cef9a --- /dev/null +++ b/lib/data/models/pending_transaction.dart @@ -0,0 +1,33 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'contact.dart'; + +part 'pending_transaction.g.dart'; + +@JsonSerializable() +@CopyWith() +class PendingTransaction extends Equatable { + const PendingTransaction({ + required this.amount, + required this.comment, + required this.time, + required this.from, + required this.to, + }); + + factory PendingTransaction.fromJson(Map<String, dynamic> json) => + _$PendingTransactionFromJson(json); + + final Contact from; + final Contact to; + final double amount; + final String comment; + final DateTime time; + + Map<String, dynamic> toJson() => _$PendingTransactionToJson(this); + + @override + List<Object?> get props => <dynamic>[from, to, amount, comment, time]; +} diff --git a/lib/data/models/pending_transaction.g.dart b/lib/data/models/pending_transaction.g.dart new file mode 100644 index 00000000..56b16107 --- /dev/null +++ b/lib/data/models/pending_transaction.g.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pending_transaction.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$PendingTransactionCWProxy { + PendingTransaction amount(double amount); + + PendingTransaction comment(String comment); + + PendingTransaction time(DateTime time); + + PendingTransaction from(Contact from); + + PendingTransaction to(Contact to); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PendingTransaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PendingTransaction(...).copyWith(id: 12, name: "My name") + /// ```` + PendingTransaction call({ + double? amount, + String? comment, + DateTime? time, + Contact? from, + Contact? to, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfPendingTransaction.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfPendingTransaction.copyWith.fieldName(...)` +class _$PendingTransactionCWProxyImpl implements _$PendingTransactionCWProxy { + const _$PendingTransactionCWProxyImpl(this._value); + + final PendingTransaction _value; + + @override + PendingTransaction amount(double amount) => this(amount: amount); + + @override + PendingTransaction comment(String comment) => this(comment: comment); + + @override + PendingTransaction time(DateTime time) => this(time: time); + + @override + PendingTransaction from(Contact from) => this(from: from); + + @override + PendingTransaction to(Contact to) => this(to: to); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PendingTransaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// PendingTransaction(...).copyWith(id: 12, name: "My name") + /// ```` + PendingTransaction call({ + Object? amount = const $CopyWithPlaceholder(), + Object? comment = const $CopyWithPlaceholder(), + Object? time = const $CopyWithPlaceholder(), + Object? from = const $CopyWithPlaceholder(), + Object? to = const $CopyWithPlaceholder(), + }) { + return PendingTransaction( + amount: amount == const $CopyWithPlaceholder() || amount == null + ? _value.amount + // ignore: cast_nullable_to_non_nullable + : amount as double, + comment: comment == const $CopyWithPlaceholder() || comment == null + ? _value.comment + // ignore: cast_nullable_to_non_nullable + : comment as String, + time: time == const $CopyWithPlaceholder() || time == null + ? _value.time + // ignore: cast_nullable_to_non_nullable + : time as DateTime, + from: from == const $CopyWithPlaceholder() || from == null + ? _value.from + // ignore: cast_nullable_to_non_nullable + : from as Contact, + to: to == const $CopyWithPlaceholder() || to == null + ? _value.to + // ignore: cast_nullable_to_non_nullable + : to as Contact, + ); + } +} + +extension $PendingTransactionCopyWith on PendingTransaction { + /// Returns a callable class that can be used as follows: `instanceOfPendingTransaction.copyWith(...)` or like so:`instanceOfPendingTransaction.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$PendingTransactionCWProxy get copyWith => + _$PendingTransactionCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PendingTransaction _$PendingTransactionFromJson(Map<String, dynamic> json) => + PendingTransaction( + amount: (json['amount'] as num).toDouble(), + comment: json['comment'] as String, + time: DateTime.parse(json['time'] as String), + from: Contact.fromJson(json['from'] as Map<String, dynamic>), + to: Contact.fromJson(json['to'] as Map<String, dynamic>), + ); + +Map<String, dynamic> _$PendingTransactionToJson(PendingTransaction instance) => + <String, dynamic>{ + 'from': instance.from, + 'to': instance.to, + 'amount': instance.amount, + 'comment': instance.comment, + 'time': instance.time.toIso8601String(), + }; diff --git a/lib/cubit/theme_cubit.dart b/lib/data/models/theme_cubit.dart similarity index 100% rename from lib/cubit/theme_cubit.dart rename to lib/data/models/theme_cubit.dart diff --git a/lib/cubit/theme_state.dart b/lib/data/models/theme_state.dart similarity index 100% rename from lib/cubit/theme_state.dart rename to lib/data/models/theme_state.dart diff --git a/lib/data/models/transactions_bloc.dart b/lib/data/models/transactions_bloc.dart new file mode 100644 index 00000000..ae01dd3b --- /dev/null +++ b/lib/data/models/transactions_bloc.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; + +import 'node_list_cubit.dart'; +import 'transaction.dart'; +import 'transaction_cubit.dart'; + +part 'transactions_state.dart'; + +class TransactionsBloc { + TransactionsBloc() { + _onPageRequest.stream + .flatMap(_fetchCharacterSummaryList) + .listen(_onNewListingStateController.add) + .addTo(_subscriptions); + + _onSearchInputChangedSubject.stream + .flatMap((_) => _resetSearch()) + .listen(_onNewListingStateController.add) + .addTo(_subscriptions); + } + + late NodeListCubit nodeListCubit; + late TransactionsCubit transCubit; + + static const int _pageSize = 20; + + final CompositeSubscription _subscriptions = CompositeSubscription(); + + final BehaviorSubject<TransactionsState> _onNewListingStateController = + BehaviorSubject<TransactionsState>.seeded( + TransactionsState(), + ); + + Stream<TransactionsState> get onNewListingState => + _onNewListingStateController.stream; + + final StreamController<String?> _onPageRequest = StreamController<String?>(); + + Sink<String?> get onPageRequestSink => _onPageRequest.sink; + + final BehaviorSubject<String?> _onSearchInputChangedSubject = + BehaviorSubject<String?>.seeded(null); + + Sink<String?> get onSearchInputChangedSink => + _onSearchInputChangedSubject.sink; + + // String? get _searchInputValue => _onSearchInputChangedSubject.value; + + Stream<TransactionsState> _resetSearch() async* { + yield TransactionsState(); + yield* _fetchCharacterSummaryList(null); + } + + void init(TransactionsCubit transCubit, NodeListCubit nodeListCubit) { + this.transCubit = transCubit; + this.nodeListCubit = nodeListCubit; + } + + Stream<TransactionsState> _fetchCharacterSummaryList(String? pageKey) async* { + final TransactionsState lastListingState = + _onNewListingStateController.value; + try { + /* final newItems = await RemoteApi.getCharacterList( + pageKey, + _pageSize, + searchTerm: _searchInputValue, + ); +*/ + final List<Transaction> newItems = await transCubit.fetchTransactions( + nodeListCubit, + cursor: pageKey, + pageSize: _pageSize); + + final bool isLastPage = newItems.length < _pageSize; + final String? nextPageKey = isLastPage ? null : transCubit.state + .endCursor; + + yield TransactionsState( + // error: null, + nextPageKey: nextPageKey, + itemList: <Transaction>[ + ...lastListingState.itemList ?? <Transaction>[], + ...newItems + ], + ); + } catch (e) { + yield TransactionsState( + error: e, + nextPageKey: lastListingState.nextPageKey, + itemList: lastListingState.itemList, + ); + } + } + + void dispose() { + _onSearchInputChangedSubject.close(); + _onNewListingStateController.close(); + _subscriptions.dispose(); + _onPageRequest.close(); + } +} diff --git a/lib/data/models/transactions_state.dart b/lib/data/models/transactions_state.dart new file mode 100644 index 00000000..beb14783 --- /dev/null +++ b/lib/data/models/transactions_state.dart @@ -0,0 +1,13 @@ +part of 'transactions_bloc.dart'; + +class TransactionsState { + TransactionsState({ + this.itemList, + this.error, + this.nextPageKey, + }); + + final List<Transaction>? itemList; + final dynamic error; + final String? nextPageKey; +} diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart index a4702622..3d401fc4 100644 --- a/lib/ui/screens/fifth_screen.dart +++ b/lib/ui/screens/fifth_screen.dart @@ -4,10 +4,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pwa_install/pwa_install.dart'; import 'package:share_plus/share_plus.dart'; -import '../../cubit/bottom_nav_cubit.dart'; -import '../../cubit/theme_cubit.dart'; import '../../data/models/app_cubit.dart'; import '../../data/models/app_state.dart'; +import '../../data/models/bottom_nav_cubit.dart'; +import '../../data/models/theme_cubit.dart'; import '../../shared_prefs.dart'; import '../notification_controller.dart'; import '../tutorial.dart'; diff --git a/lib/ui/screens/first_screen.dart b/lib/ui/screens/first_screen.dart index 7a05603f..ea4135e3 100644 --- a/lib/ui/screens/first_screen.dart +++ b/lib/ui/screens/first_screen.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_browser_detect/web_browser_detect.dart'; -import '../../cubit/bottom_nav_cubit.dart'; import '../../data/models/app_cubit.dart'; import '../../data/models/app_state.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../../data/models/payment_cubit.dart'; import '../../data/models/payment_state.dart'; import '../../data/models/transaction_cubit.dart'; diff --git a/lib/ui/screens/fourth_screen.dart b/lib/ui/screens/fourth_screen.dart index ee985635..b3eb58d2 100644 --- a/lib/ui/screens/fourth_screen.dart +++ b/lib/ui/screens/fourth_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../cubit/bottom_nav_cubit.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../tutorial.dart'; import '../widgets/fourth_screen/fourth_tutorial.dart'; import '../widgets/fourth_screen/transaction_page.dart'; diff --git a/lib/ui/screens/second_screen.dart b/lib/ui/screens/second_screen.dart index ebf78b28..6bd36762 100644 --- a/lib/ui/screens/second_screen.dart +++ b/lib/ui/screens/second_screen.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../cubit/bottom_nav_cubit.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../tutorial.dart'; import '../widgets/card_drawer.dart'; import '../widgets/second_screen/card_terminal.dart'; diff --git a/lib/ui/screens/skeleton_screen.dart b/lib/ui/screens/skeleton_screen.dart index 8b55b9fb..817ac515 100644 --- a/lib/ui/screens/skeleton_screen.dart +++ b/lib/ui/screens/skeleton_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../cubit/bottom_nav_cubit.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../widgets/app_bar_gone.dart'; import '../widgets/bottom_nav_bar.dart'; import 'fifth_screen.dart'; diff --git a/lib/ui/screens/third_screen.dart b/lib/ui/screens/third_screen.dart index 04c00866..bb287ffe 100644 --- a/lib/ui/screens/third_screen.dart +++ b/lib/ui/screens/third_screen.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../cubit/bottom_nav_cubit.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../../data/models/contact.dart'; import '../../data/models/contact_cubit.dart'; import '../../g1/g1_helper.dart'; @@ -27,9 +27,7 @@ class _ThirdScreenState extends State<ThirdScreen> { @override void initState() { tutorial = ThirdTutorial(context); - if (context - .read<BottomNavCubit>() - .state == 2) { + if (context.read<BottomNavCubit>().state == 2) { Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); } super.initState(); @@ -46,7 +44,7 @@ class _ThirdScreenState extends State<ThirdScreen> { final String? pubKey = await QrManager.qrScan(context); if (pubKey != null && validateKey(pubKey)) { final Contact contact = - await ContactsCache().getContact(pubKey); + await ContactsCache().getContact(pubKey); if (!mounted) { return; } diff --git a/lib/ui/widgets/bottom_nav_bar.dart b/lib/ui/widgets/bottom_nav_bar.dart index d7a598b6..f742dcb0 100644 --- a/lib/ui/widgets/bottom_nav_bar.dart +++ b/lib/ui/widgets/bottom_nav_bar.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../cubit/bottom_nav_cubit.dart'; +import '../../data/models/bottom_nav_cubit.dart'; import '../tutorial_keys.dart'; class BottomNavBar extends StatefulWidget { diff --git a/lib/ui/widgets/first_screen/pay_form.dart b/lib/ui/widgets/first_screen/pay_form.dart index b4ecf679..8322a62b 100644 --- a/lib/ui/widgets/first_screen/pay_form.dart +++ b/lib/ui/widgets/first_screen/pay_form.dart @@ -62,8 +62,8 @@ class _PayFormState extends State<PayForm> { fontSize: 16, ), ); - final Widget payBtnText = Text(tr('g1_form_pay_send') + - (!kReleaseMode ? ' ${state.amount} ${state.comment}' : '')); + final Widget payBtnText = Text( + tr('g1_form_pay_send') + (!kReleaseMode ? ' ${state.status}' : '')); return Form( key: _formKey, child: Column( diff --git a/lib/ui/widgets/first_screen/theme_card.dart b/lib/ui/widgets/first_screen/theme_card.dart index 678f0a9f..16c44e48 100644 --- a/lib/ui/widgets/first_screen/theme_card.dart +++ b/lib/ui/widgets/first_screen/theme_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../cubit/theme_cubit.dart'; +import '../../../data/models/theme_cubit.dart'; class ThemeCard extends StatelessWidget { const ThemeCard({ diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart index 05fce950..82c56a41 100644 --- a/lib/ui/widgets/fourth_screen/transaction_page.dart +++ b/lib/ui/widgets/fourth_screen/transaction_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:backdrop/backdrop.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -10,8 +12,8 @@ import '../../../data/models/node_list_cubit.dart'; import '../../../data/models/transaction.dart'; import '../../../data/models/transaction_balance_state.dart'; import '../../../data/models/transaction_cubit.dart'; +import '../../../data/models/transactions_bloc.dart'; import '../../../shared_prefs.dart'; -import '../../logger.dart'; import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'transaction_chart.dart'; @@ -29,11 +31,11 @@ class _TransactionsAndBalanceWidgetState extends State<TransactionsAndBalanceWidget> with SingleTickerProviderStateMixin { final ScrollController _transScrollController = ScrollController(); - + final TransactionsBloc _bloc = TransactionsBloc(); + late StreamSubscription<TransactionsState> _blocListingStateSubscription; late NodeListCubit nodeListCubit; late TransactionsCubit transCubit; bool isLoading = false; - static const int _pageSize = 20; final PagingController<String?, Transaction> _pagingController = PagingController<String?, Transaction>(firstPageKey: null); @@ -43,13 +45,31 @@ class _TransactionsAndBalanceWidgetState // Remove in the future transCubit = context.read<TransactionsCubit>(); nodeListCubit = context.read<NodeListCubit>(); + _bloc.init(transCubit, nodeListCubit); + _pagingController.addPageRequestListener((String? cursor) { + _bloc.onPageRequestSink.add(cursor); + }); + // We could've used StreamBuilder, but that would unnecessarily recreate + // the entire [PagedSliverGrid] every time the state changes. + // Instead, handling the subscription ourselves and updating only the + // _pagingController is more efficient. + _blocListingStateSubscription = + _bloc.onNewListingState.listen((TransactionsState listingState) { + _pagingController.value = PagingState<String?, Transaction>( + nextPageKey: listingState.nextPageKey, + error: listingState.error, + itemList: listingState.itemList, + ); + }); + + /* _pagingController.addPageRequestListener((String? cursor) { EasyThrottle.throttle('my-throttler-$cursor', const Duration(seconds: 1), () => _fetchPage(cursor), onAfter: () {} // <-- Optional callback, called after the duration has passed ); - }); + }); */ _pagingController.addStatusListener((PagingStatus status) { if (status == PagingStatus.subsequentPageError) { ScaffoldMessenger.of(context).showSnackBar( @@ -67,7 +87,7 @@ class _TransactionsAndBalanceWidgetState super.initState(); } - Future<void> _fetchPage(String? cursor) async { +/* Future<void> _fetchPage(String? cursor) async { logger('Fetching from transaction page with cursor $cursor'); try { final List<Transaction> newItems = await transCubit.fetchTransactions( @@ -85,12 +105,13 @@ class _TransactionsAndBalanceWidgetState } catch (error) { _pagingController.error = error; } - } + }*/ @override void dispose() { _transScrollController.dispose(); _pagingController.dispose(); + _blocListingStateSubscription.cancel(); super.dispose(); } diff --git a/lib/ui/widgets/third_screen/contacts_page.dart b/lib/ui/widgets/third_screen/contacts_page.dart index b23c2362..be4eb126 100644 --- a/lib/ui/widgets/third_screen/contacts_page.dart +++ b/lib/ui/widgets/third_screen/contacts_page.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:share_plus/share_plus.dart'; -import '../../../cubit/bottom_nav_cubit.dart'; +import '../../../data/models/bottom_nav_cubit.dart'; import '../../../data/models/contact.dart'; import '../../../data/models/contact_cubit.dart'; import '../../../data/models/payment_cubit.dart'; diff --git a/pubspec.lock b/pubspec.lock index ad07e2eb..c5f143a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1159,7 +1159,7 @@ packages: source: hosted version: "0.2.0" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" diff --git a/pubspec.yaml b/pubspec.yaml index 24745e3a..2680b08d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: feedback_sentry: ^2.4.0 feedback_gitlab: ^2.2.0 connectivity_wrapper: ^1.1.3 + rxdart: ^0.27.7 dev_dependencies: flutter_test: -- GitLab