Flutter refactoring to BLoC / Cubit

46 views Asked by At

I'm working on refactoring an app and try to apply BLoC / Cubit architecture.

The code provided works fine, but I need your advice to improve this refactoring according to BLoC principles, before I start to refactor all the project this way. My main goals are to make it easy to developers to switch to this new architecture and as readable as possible.

original version spot_widget.dart

import 'package:project/models/latlon.dart';
import 'package:project/models/spot_info.dart';
import 'package:project/models/spot_review.dart';
import 'package:flutter/widgets.dart';

class SpotWidget extends StatefulWidget {
  final LatLon location;
  const SpotWidget({super.key, required this.location});

  @override
  State<SpotWidget> createState() => _SpotWidgetState();
}

class _SpotWidgetState extends State<SpotWidget> {
  SpotInfo? info;
  List<SpotReview> reviews = [];

  Future<void> _loadInfo() async {
    final infos = await SpotInfo.get(widget.location);
    info = infos.first;
    setState(() {});
  }

  Future<void> _loadReviews() async {
    reviews = await SpotReview.get(widget.location);
    setState(() {});
  }

  @override
  void initState() {
    _loadInfo();
    _loadReviews();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(widget.location.toString()),
        // TODO: Widget to display all informations about a spot
        if (info != null) Text(info!.isPublic.toString()),
        // TODO: Widget to display all reviews about a spot
        if (reviews.isNotEmpty) Text(reviews.length.toString()),
      ],
    );
  }
}

BLoC refactoring

spot_repository.dart

import 'package:project/models/latlon.dart';
import 'package:project/models/spot_info.dart';
import 'package:project/models/spot_review.dart';

class SpotRepository {
  Future<SpotInfo> getSpotInfo(LatLon location) async {
    final infos = await SpotInfo.get(location);
    return infos.first;
  }

  Future<List<SpotReview>> getSpotReviews(LatLon location) async {
    return await SpotReview.get(location);
  }
}

spot_cubit.dart

import 'package:project/models/latlon.dart';
import 'package:project/spot/spot_repository.dart';
import 'package:project/spot/spot_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SpotCubit extends Cubit<SpotState> {
  final SpotRepository _repository;

  SpotCubit(this._repository) : super(SpotInitial());

  Future<void> loadSpotInfoAndReviews(LatLon location) async {
    emit(SpotLoading());
    try {
      final info = await _repository.getSpotInfo(location);
      final reviews = await _repository.getSpotReviews(location);
      emit(SpotLoaded(info, reviews));
    } catch (e) {
      emit(SpotError("Failed to load spot info and reviews"));
    }
  }
}

spot_state.dart

import 'package:project/models/spot_info.dart';
import 'package:project/models/spot_review.dart';
import 'package:flutter/material.dart';

@immutable
abstract class SpotState {}

class SpotInitial extends SpotState {}

class SpotLoading extends SpotState {}

class SpotLoaded extends SpotState {
  final SpotInfo info;
  final List<SpotReview> reviews;

  SpotLoaded(this.info, this.reviews);
}

class SpotError extends SpotState {
  final String message;

  SpotError(this.message);
}

spot_widget.dart

import 'package:project/models/latlon.dart';
import 'package:project/spot/spot_cubit.dart';
import 'package:project/spot/spot_repository.dart';
import 'package:project/spot/spot_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SpotBlocWidget extends StatelessWidget {
  final LatLon location;

  const SpotBlocWidget({super.key, required this.location});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) =>
          SpotCubit(SpotRepository())..loadSpotInfoAndReviews(location),
      child: BlocBuilder<SpotCubit, SpotState>(
        builder: (context, state) {
          if (state is SpotLoading) {
            return const CircularProgressIndicator();
          } else if (state is SpotLoaded) {
            return Column(
              children: [
                Text(location.toString()),
                Text(state.info.isPublic.toString()),
                Text(state.reviews.length.toString()),
              ],
            );
          } else if (state is SpotError) {
            return Text(state.message);
          } else {
            return const Text('Something went wrong');
          }
        },
      ),
    );
  }
}
1

There are 1 answers

0
Vishnu Priyan S S On

Everything looks good. But as per Bloc/Cubit there will be lot of boilerplate so it will be more scalable and easy to modify. In spot_cubit.dart it is advised to separate both repository call into different functions, will be helpful to handle error and scale in the future like below. It will also be easy to change from cubit to Bloc when needed. As Spot has multiple callings, changing it into a bloc is advised but not a must, this depends on it need to be scales in the future. If pagination need to be added to the spots list, separating this now will help in adding pagination feature.

import 'package:project/models/latlon.dart';
import 'package:project/spot/spot_repository.dart';
import 'package:project/spot/spot_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SpotCubit extends Cubit < SpotState > {
  final SpotRepository _repository;

  SpotCubit(this._repository): super(SpotInitial());

  Future < void > loadSpotReviews(LatLon location) async {
    emit(SpotLoading());
    try {
      final reviews = await _repository.getSpotReviews(location);
      emit(SpotReviewLoaded(reviews));
    } catch (e) {
      emit(SpotError("Failed to load spot info and reviews"));
    }
  }
  Future < void > loadSpotInfo(LatLon location) async {
    emit(SpotLoading());
    try {
      final info = await _repository.getSpotInfo(location);
      emit(SpotInfoLoaded(info));
    } catch (e) {
      emit(SpotError("Failed to load spot info and reviews"));
    }
  }
}

In spot_widget.dart each state's widget can extracted to separated method and placed in new file like

Widget SpotLoadedWidget(LatLong location, SpotState state){
return Column(
              children: [
                Text(location.toString()),
                Text(state.info.isPublic.toString()),
                Text(state.reviews.length.toString()),
              ],
            )
}

And Error widget to separate file so that it can be reused. In Bloc/Cubit, a lot of code means, it can scaled and reused with ease.