Hydrated Bloc In Flutter

Hydrated Bloc In Flutter

main.dart

import 'dart:async';

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

import 'hydrated_bloc.dart';

void main() async {
  BlocSupervisor.delegate = await HydratedBlocDelegate.build();
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<CounterBloc>(
      builder: (context) => CounterBloc(),
      child: MaterialApp(
        title: 'Flutter Tutorial Counter',
        home: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterEvent, CounterState>(
        bloc: counterBloc,
        builder: (BuildContext context, CounterState state) {
          return Center(
            child: Text(
              '${state.value}',
              style: TextStyle(fontSize: 24.0),
            ),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                counterBloc.dispatch(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
                counterBloc.dispatch(CounterEvent.decrement);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.delete_forever),
              onPressed: () async {
                await (BlocSupervisor.delegate as HydratedBlocDelegate)
                    .storage
                    .clear();
              },
            ),
          ),
        ],
      ),
    );
  }
}

enum CounterEvent { increment, decrement }

class CounterState {
  int value;

  CounterState(this.value);

  @override
  String toString() => 'CounterState { value: $value }';
}

class CounterBloc extends HydratedBloc<CounterEvent, CounterState> {
  @override
  CounterState get initialState => super.initialState ?? CounterState(0);

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield CounterState(currentState.value - 1);
        break;
      case CounterEvent.increment:
        yield CounterState(currentState.value + 1);
        break;
    }
  }

  @override
  CounterState fromJson(Map<String, dynamic> source) {
    return CounterState(source['value'] as int);
  }

  @override
  Map<String, int> toJson(CounterState state) {
    return {'value': state.value};
  }
}

hydrated_bloc.dart

library hydrated_bloc;

export 'src/hydrated_bloc_delegate.dart';
export 'src/hydrated_bloc_storage.dart';
export 'src/hydrated_bloc.dart';

hydrated_bloc.dart

import 'dart:convert';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';

/// Specialized `Bloc` which handles initializing the `Bloc` state
/// based on the persisted state. This allows state to be persisted
/// across hot restarts as well as complete app restarts.
abstract class HydratedBloc<Event, State> extends Bloc<Event, State> {
  final HydratedStorage storage =
      (BlocSupervisor.delegate as HydratedBlocDelegate).storage;

  @mustCallSuper
  @override
  State get initialState {
    try {
      return fromJson(
        json.decode(
          storage?.read(this.runtimeType.toString()) as String,
        ) as Map<String, dynamic>,
      );
    } catch (_) {
      return null;
    }
  }

  /// Responsible for converting the `Map<String, dynamic>` representation of the bloc state
  /// into a concrete instance of the bloc state.
  ///
  /// If `fromJson` throws an `Exception`, `HydratedBloc` will return an `initialState` of `null`
  /// so it is recommended to set `initialState` in the bloc to `super.initialState() ?? defaultInitialState()`.
  State fromJson(Map<String, dynamic> json);

  /// Responsible for converting a concrete instance of the bloc state
  /// into the the `Map<String, dynamic>` representation.
  ///
  /// If `toJson` returns `null`, then no state changes will be persisted.
  Map<String, dynamic> toJson(State state);
}

pubspec.yaml

name: hydrated_bloc
description: A new Flutter application.

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons:
  bloc:  ^0.14.0
  meta:
  path_provider:
  mockito:
  flutter_bloc: ^0.17.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

hydrated_bloc.dart

import 'dart:convert';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';

/// Specialized `Bloc` which handles initializing the `Bloc` state
/// based on the persisted state. This allows state to be persisted
/// across hot restarts as well as complete app restarts.
abstract class HydratedBloc<Event, State> extends Bloc<Event, State> {
  final HydratedStorage storage =
      (BlocSupervisor.delegate as HydratedBlocDelegate).storage;

  @mustCallSuper
  @override
  State get initialState {
    try {
      return fromJson(
        json.decode(
          storage?.read(this.runtimeType.toString()) as String,
        ) as Map<String, dynamic>,
      );
    } catch (_) {
      return null;
    }
  }

  /// Responsible for converting the `Map<String, dynamic>` representation of the bloc state
  /// into a concrete instance of the bloc state.
  ///
  /// If `fromJson` throws an `Exception`, `HydratedBloc` will return an `initialState` of `null`
  /// so it is recommended to set `initialState` in the bloc to `super.initialState() ?? defaultInitialState()`.
  State fromJson(Map<String, dynamic> json);

  /// Responsible for converting a concrete instance of the bloc state
  /// into the the `Map<String, dynamic>` representation.
  ///
  /// If `toJson` returns `null`, then no state changes will be persisted.
  Map<String, dynamic> toJson(State state);
}

hydrated_bloc_delegate.dart

import 'dart:async';
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

/// A specialized `BlocDelegate` which handles persisting state changes
/// transparently and asynchronously.
class HydratedBlocDelegate extends BlocDelegate {
  /// Instance of `HydratedStorage` used to manage persisted states.
  final HydratedStorage storage;

  /// Builds a new instance of `HydratedBlocDelegate` with the
  /// default `HydratedBlocStorage`.
  ///
  /// This is the recommended way to use a `HydratedBlocDelegate`.
  /// If you want to customize `HydratedBlocDelegate` you can extend `HydratedBlocDelegate`
  /// and perform the necessary overrides.
  static Future<HydratedBlocDelegate> build() async {
    return HydratedBlocDelegate(await HydratedBlocStorage.getInstance());
  }

  HydratedBlocDelegate(this.storage);

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    final dynamic state = transition.nextState;
    if (bloc is HydratedBloc) {
      final stateJson = bloc.toJson(state);
      if (stateJson != null) {
        storage.write(
          bloc.runtimeType.toString(),
          json.encode(stateJson),
        );
      }
    }
  }
}

hydrated_bloc_storage.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

/// Interface which `HydratedBlocDelegate` uses to persist and retrieve
/// state changes from the local device.
abstract class HydratedStorage {
  /// Returns value for key
  dynamic read(String key);

  /// Persists key value pair
  Future<void> write(String key, dynamic value);

  /// Clears all key value pairs from storage
  Future<void> clear();
}

/// Implementation of `HydratedStorage` which uses `PathProvider` and `dart.io`
/// to persist and retrieve state changes from the local device.
class HydratedBlocStorage implements HydratedStorage {
  static const String _hydratedBlocStorageName = '.hydrated_bloc.json';
  static HydratedBlocStorage _instance;
  Map<String, dynamic> _storage;
  File _file;

  /// Returns an instance of `HydratedBlocStorage`.
  static Future<HydratedBlocStorage> getInstance() async {
    if (_instance != null) {
      return _instance;
    }

    final Directory directory = await getTemporaryDirectory();
    final File file = File('${directory.path}/$_hydratedBlocStorageName');
    Map<String, dynamic> storage = Map<String, dynamic>();

    if (await file.exists()) {
      try {
        storage =
            json.decode(await file.readAsString()) as Map<String, dynamic>;
      } catch (_) {
        await file.delete();
      }
    }

    _instance = HydratedBlocStorage._(storage, file);
    return _instance;
  }

  HydratedBlocStorage._(this._storage, this._file);

  @override
  dynamic read(String key) {
    return _storage[key];
  }

  @override
  Future<void> write(String key, dynamic value) async {
    _storage[key] = value;
    await _file.writeAsString(json.encode(_storage));
    return _storage[key] = value;
  }

  @override
  Future<void> clear() async {
    _storage = Map<String, dynamic>();
    _instance = null;
    return await _file.exists() ? await _file.delete() : null;
  }
}