Browse Source

fix(neon): remove and dispose dagnling blocs after logout

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/721/head
Nikolas Rimikis 1 year ago
parent
commit
a5a99a34e6
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 18
      packages/neon/neon/lib/src/bloc/bloc.dart
  2. 53
      packages/neon/neon/lib/src/blocs/accounts.dart
  3. 41
      packages/neon/neon/lib/src/models/account_cache.dart
  4. 7
      packages/neon/neon/lib/src/models/app_implementation.dart
  5. 33
      packages/neon/neon/lib/src/models/disposable.dart
  6. 3
      packages/neon/neon/lib/src/settings/models/option.dart
  7. 8
      packages/neon/neon/lib/src/settings/models/options_collection.dart
  8. 59
      packages/neon/neon/test/account_cache_test.dart
  9. 39
      packages/neon/neon/test/disposable_test.dart

18
packages/neon/neon/lib/src/bloc/bloc.dart

@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:neon/src/utils/request_manager.dart';
abstract class Bloc {
abstract class Bloc implements Disposable {
@override
@mustCallSuper
void dispose();
}
@ -44,17 +46,3 @@ abstract class InteractiveBloc extends Bloc {
}
}
}
extension DisposeableIterableBloc on Iterable<Bloc> {
void disposeAll() {
for (final bloc in this) {
bloc.dispose();
}
}
}
extension DisposeableMapBloc on Map<dynamic, Bloc> {
void disposeAll() {
values.disposeAll();
}
}

53
packages/neon/neon/lib/src/blocs/accounts.dart

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/bloc/bloc.dart';
@ -10,6 +11,7 @@ import 'package:neon/src/blocs/unified_search.dart';
import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_statuses.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/account_cache.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/utils/account_options.dart';
@ -97,30 +99,40 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
setActiveAccount(as.first);
}
}
accounts.listen((final accounts) {
_accountsOptions.pruneAgainst(accounts);
_appsBlocs.pruneAgainst(accounts);
_capabilitiesBlocs.pruneAgainst(accounts);
_userDetailsBlocs.pruneAgainst(accounts);
_userStatusesBlocs.pruneAgainst(accounts);
_unifiedSearchBlocs.pruneAgainst(accounts);
for (final app in _allAppImplementations) {
app.blocsCache.pruneAgainst(accounts);
}
});
}
final GlobalOptions _globalOptions;
final Iterable<AppImplementation> _allAppImplementations;
final _accountsOptions = <String, AccountSpecificOptions>{};
final _appsBlocs = <String, AppsBloc>{};
final _capabilitiesBlocs = <String, CapabilitiesBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusesBlocs = <String, UserStatusesBloc>{};
final _unifiedSearchBlocs = <String, UnifiedSearchBloc>{};
final _accountsOptions = AccountCache<AccountSpecificOptions>();
final _appsBlocs = AccountCache<AppsBloc>();
final _capabilitiesBlocs = AccountCache<CapabilitiesBloc>();
final _userDetailsBlocs = AccountCache<UserDetailsBloc>();
final _userStatusesBlocs = AccountCache<UserStatusesBloc>();
final _unifiedSearchBlocs = AccountCache<UnifiedSearchBloc>();
@override
void dispose() {
unawaited(activeAccount.close());
unawaited(accounts.close());
_appsBlocs.disposeAll();
_capabilitiesBlocs.disposeAll();
_userDetailsBlocs.disposeAll();
_userStatusesBlocs.disposeAll();
_unifiedSearchBlocs.disposeAll();
for (final options in _accountsOptions.values) {
options.dispose();
}
_appsBlocs.dispose();
_capabilitiesBlocs.dispose();
_userDetailsBlocs.dispose();
_userStatusesBlocs.dispose();
_unifiedSearchBlocs.dispose();
_accountsOptions.dispose();
}
@override
@ -212,8 +224,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// The options for the specified [account].
///
/// Use [activeOptions] to get them for the [activeAccount].
AccountSpecificOptions getOptionsFor(final Account account) =>
_accountsOptions[account.id] ??= AccountSpecificOptions(
AccountSpecificOptions getOptionsFor(final Account account) => _accountsOptions[account] ??= AccountSpecificOptions(
AppStorage(StorageKeys.accounts, account.id),
getAppsBlocFor(account),
);
@ -226,7 +237,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// The appsBloc for the specified [account].
///
/// Use [activeAppsBloc] to get them for the [activeAccount].
AppsBloc getAppsBlocFor(final Account account) => _appsBlocs[account.id] ??= AppsBloc(
AppsBloc getAppsBlocFor(final Account account) => _appsBlocs[account] ??= AppsBloc(
getCapabilitiesBlocFor(account),
this,
account,
@ -242,7 +253,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// Use [activeCapabilitiesBloc] to get them for the [activeAccount].
CapabilitiesBloc getCapabilitiesBlocFor(final Account account) =>
_capabilitiesBlocs[account.id] ??= CapabilitiesBloc(account);
_capabilitiesBlocs[account] ??= CapabilitiesBloc(account);
/// The userDetailsBloc for the [activeAccount].
///
@ -253,7 +264,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// Use [activeUerDetailsBloc] to get them for the [activeAccount].
UserDetailsBloc getUserDetailsBlocFor(final Account account) =>
_userDetailsBlocs[account.id] ??= UserDetailsBloc(account);
_userDetailsBlocs[account] ??= UserDetailsBloc(account);
/// The userStatusBloc for the [activeAccount].
///
@ -264,7 +275,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// Use [activeUserStatusesBloc] to get them for the [activeAccount].
UserStatusesBloc getUserStatusesBlocFor(final Account account) =>
_userStatusesBlocs[account.id] ??= UserStatusesBloc(account);
_userStatusesBlocs[account] ??= UserStatusesBloc(account);
/// The UnifiedSearchBloc for the [activeAccount].
///
@ -275,7 +286,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// Use [activeUnifiedSearchBloc] to get them for the [activeAccount].
UnifiedSearchBloc getUnifiedSearchBlocFor(final Account account) =>
_unifiedSearchBlocs[account.id] ??= UnifiedSearchBloc(
_unifiedSearchBlocs[account] ??= UnifiedSearchBloc(
getAppsBlocFor(account),
account,
);

41
packages/neon/neon/lib/src/models/account_cache.dart

@ -0,0 +1,41 @@
import 'package:neon/models.dart';
import 'package:neon/src/models/disposable.dart';
/// Cache for [Account] specific [Disposable] objects.
class AccountCache<T extends Disposable> implements Disposable {
AccountCache();
final Map<String, T> _cache = {};
/// Disposes all entries and clears the cache.
@override
void dispose() {
_cache
..disposeAll()
..clear();
}
/// Updates the cache against the given [accounts].
///
/// Every cache entry with an account no longer in [accounts] is removed and
/// disposed. This method is in O(N²).
void pruneAgainst(final Iterable<Account> accounts) {
_cache.removeWhere((final key, final value) {
if (accounts.tryFind(key) == null) {
value.dispose();
return true;
}
return false;
});
}
/// The value for the given [account], or `null` if [account] is not in the cache.
T? operator [](final Account account) => _cache[account.id];
/// Associates the [account] with the given [value].
///
/// If the account was already in the cache, its associated value is changed.
/// Otherwise the account/value pair is added to the cache.
void operator []=(final Account account, final T value) => _cache[account.id] = value;
}

7
packages/neon/neon/lib/src/models/app_implementation.dart

@ -6,6 +6,7 @@ import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/account_cache.dart';
import 'package:neon/src/settings/models/options_collection.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/widgets/drawer_destination.dart';
@ -30,9 +31,9 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
@mustBeOverridden
R get options;
final Map<String, T> _blocs = {};
final blocsCache = AccountCache<T>();
T getBloc(final Account account) => _blocs[account.id] ??= buildBloc(account);
T getBloc(final Account account) => blocsCache[account] ??= buildBloc(account);
@protected
T buildBloc(final Account account);
@ -113,7 +114,7 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
@mustCallSuper
void dispose() {
options.dispose();
_blocs.disposeAll();
blocsCache.dispose();
}
/// A custom theme that will be injected into the widget tree.

33
packages/neon/neon/lib/src/models/disposable.dart

@ -0,0 +1,33 @@
import 'package:meta/meta.dart';
/// Interface of a disposable class.
abstract interface class Disposable {
/// Discards any resources used by the object. After this is called, the
/// object is not in a usable state and should be discarded (calls to
/// streams or other state should throw after the object is disposed).
///
/// This method should only be called by the object's owner.
@mustCallSuper
void dispose();
}
extension DisposeableIterableBloc on Iterable<Disposable> {
/// Calls [Disposable.dispose] on all entries.
///
/// The disposed values will not be removed from the iteraable.
void disposeAll() {
for (final bloc in this) {
bloc.dispose();
}
}
}
extension DisposeableMapBloc on Map<dynamic, Disposable> {
/// Calls [Disposable.dispose] on all entries.
///
/// The disposed values will not be removed from the map.
/// Call [clear] to remove them.
void disposeAll() {
values.disposeAll();
}
}

3
packages/neon/neon/lib/src/settings/models/option.dart

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:neon/src/models/label_builder.dart';
import 'package:neon/src/settings/models/options_category.dart';
import 'package:neon/src/settings/models/storage.dart';
@ -13,7 +14,7 @@ import 'package:rxdart/rxdart.dart';
/// See:
/// * [ToggleOption] for an Option<bool>
/// * [SelectOption] for an Option with multiple values
sealed class Option<T> extends ChangeNotifier implements ValueListenable<T> {
sealed class Option<T> extends ChangeNotifier implements ValueListenable<T>, Disposable {
/// Creates an Option
Option({
required this.storage,

8
packages/neon/neon/lib/src/settings/models/options_collection.dart

@ -1,11 +1,12 @@
import 'package:meta/meta.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:neon/src/settings/models/exportable.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/options_category.dart';
import 'package:neon/src/settings/models/storage.dart';
/// Collection of [Option]s.
abstract class OptionsCollection implements Exportable {
abstract class OptionsCollection implements Exportable, Disposable {
OptionsCollection(this.storage);
/// Storage backend to use.
@ -30,10 +31,9 @@ abstract class OptionsCollection implements Exportable {
///
/// Implementers extending this must call super.
@mustCallSuper
@override
void dispose() {
for (final option in options) {
option.dispose();
}
options.disposeAll();
}
@override

59
packages/neon/neon/test/account_cache_test.dart

@ -0,0 +1,59 @@
import 'package:mocktail/mocktail.dart';
import 'package:neon/models.dart';
import 'package:neon/src/models/account_cache.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:test/test.dart';
class DisposableMock extends Mock implements Disposable {}
// ignore: avoid_implementing_value_types
class AccountMock extends Mock implements Account {}
void main() {
final disposable0 = DisposableMock();
final disposable1 = DisposableMock();
final account0 = AccountMock();
final account1 = AccountMock();
when(() => account0.id).thenReturn('key0');
when(() => account1.id).thenReturn('key1');
group('AccountCache', () {
test('map functionality', () {
final cache = AccountCache<DisposableMock>();
expect(cache[account0], isNull);
cache[account0] = disposable0;
expect(cache[account0], disposable0);
expect(cache[account0] ??= disposable1, disposable0);
expect(cache[account1] ??= disposable1, disposable1);
});
test('prune', () {
final cache = AccountCache<DisposableMock>();
cache[account0] = disposable0;
cache[account1] = disposable1;
cache.pruneAgainst([account0]);
expect(cache[account0], disposable0);
expect(cache[account1], isNull);
verify(disposable1.dispose).called(1);
});
test('dispose', () {
final cache = AccountCache<DisposableMock>();
cache[account0] = disposable0;
cache[account1] = disposable1;
cache.dispose();
expect(cache[account0], isNull);
expect(cache[account1], isNull);
verify(disposable0.dispose).called(1);
verify(disposable1.dispose).called(1);
});
});
}

39
packages/neon/neon/test/disposable_test.dart

@ -0,0 +1,39 @@
// ignore_for_file: cascade_invocations
import 'package:mocktail/mocktail.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:test/test.dart';
class DisposableMock extends Mock implements Disposable {}
void main() {
test('Disposable extensions', () {
final disposable0 = DisposableMock();
final disposable1 = DisposableMock();
final disposable3 = DisposableMock();
final list = [
disposable0,
disposable1,
disposable3,
];
list.disposeAll();
verify(disposable0.dispose).called(1);
verify(disposable1.dispose).called(1);
verify(disposable3.dispose).called(1);
final map = {
'disposable0': disposable0,
'disposable1': disposable1,
'disposable3': disposable3,
};
map.disposeAll();
verify(disposable0.dispose).called(1);
verify(disposable1.dispose).called(1);
verify(disposable3.dispose).called(1);
});
}
Loading…
Cancel
Save