Skip to content

Commit

Permalink
Merge pull request #37 from Nuxify/feature/refactor-api-cubit
Browse files Browse the repository at this point in the history
feat: refactor api cubit
  • Loading branch information
kabaluyot authored Oct 31, 2024
2 parents 36fee6a + 87d82cb commit c3d4d1f
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 117 deletions.
49 changes: 49 additions & 0 deletions lib/core/application/service/cubit/quote_api_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:flirt/core/application/service/cubit/quote_dto.dart';
import 'package:flirt/core/domain/models/api_error_response.dart';
import 'package:flirt/core/domain/models/api_response.dart';
import 'package:flirt/core/domain/models/quote/quote_response.dart';
import 'package:flirt/core/domain/repository/quote_repository.dart';
import 'package:flirt/internal/utils.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'quote_api_state.dart';

class QuoteAPICubit extends Cubit<QuoteAPIState> {
QuoteAPICubit({required this.quoteRepository}) : super(QuoteAPIState());
final IQuoteRepository quoteRepository;

/// Get Quote
Future<void> fetchQuote() async {
try {
emit(FetchQuoteLoading());

final APIListResponse<QuoteResponse> response =
await quoteRepository.fetchQuote();

final QuoteResponseDTO quote = QuoteResponseDTO(
author: response.data.first.author,
content: response.data.first.quote,
);

// emit FetchQuoteSuccess event
// this set `quotes` in base class and emits an event
emit(FetchQuoteSuccess(quote));
} on APIErrorResponse catch (error) {
emit(
FetchQuoteFailed(
errorCode: error.errorCode!,
message: error.message,
),
);
} catch (e, stackTrace) {
logError(e, stackTrace);

emit(
FetchQuoteFailed(
errorCode: '$e',
message: 'Something went wrong.',
),
);
}
}
}
21 changes: 21 additions & 0 deletions lib/core/application/service/cubit/quote_api_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
part of 'quote_api_cubit.dart';

class QuoteAPIState {}

class FetchQuoteLoading extends QuoteAPIState {}

class FetchQuoteSuccess extends QuoteAPIState {
FetchQuoteSuccess(this.quoteResponse);

final QuoteResponseDTO quoteResponse;
}

class FetchQuoteFailed extends QuoteAPIState {
FetchQuoteFailed({
required this.errorCode,
required this.message,
});

final String errorCode;
final String message;
}
65 changes: 11 additions & 54 deletions lib/core/module/home/application/service/cubit/home_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import 'package:flirt/core/domain/models/api_error_response.dart';
import 'package:flirt/core/domain/models/api_response.dart';
import 'package:flirt/core/domain/models/quote/quote_response.dart';
import 'package:flirt/core/domain/repository/quote_repository.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_dto.dart';
import 'package:flirt/internal/utils.dart';
import 'package:flirt/core/application/service/cubit/quote_dto.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'home_state.dart';

class HomeCubit extends Cubit<HomeState> {
HomeCubit({required this.quoteRepository})
HomeCubit()
: super(
HomeState(
data: QuoteStateDTO(
Expand All @@ -19,52 +14,14 @@ class HomeCubit extends Cubit<HomeState> {
),
);

final IQuoteRepository quoteRepository;

/// Get Quote
Future<void> fetchQuote() async {
try {
/// Persist data inside state by emitting the default value of state. If not, it will override the value.
emit(FetchQuoteLoading(state.data));

final APIListResponse<QuoteResponse> response =
await quoteRepository.fetchQuote();
final QuoteResponseDTO quote = QuoteResponseDTO(
author: response.data.first.author,
content: response.data.first.quote,
);

/// Appending new value inside the list of the properties of DTO.
state.data.quotes.add(quote);
state.data.authors.add(quote.author);

// emit FetchQuoteSuccess event
// this set `quotes` in base class and emits an event
emit(FetchQuoteSuccess(state.data, quote));

/// This approach is used to demonstrate persistent data storing that you might use on cases where you need to hold data for a series of screens in a module.
/// For example: If you separate a lengthy registration form in to 5 separate screens, you would need to hold that data until the last step.
// If not for demonstration purposes, this approach should be using code below - emitting state directly.
// emit(QuoteState(data: QuoteStateDTO(quotes: <QuoteResponseDTO>[], authors: <String>[])));
} on APIErrorResponse catch (error) {
emit(
FetchQuoteFailed(
errorCode: error.errorCode!,
message: error.message,
data: state.data,
),
);
} catch (e, stackTrace) {
logError(e, stackTrace);

emit(
FetchQuoteFailed(
errorCode: '$e',
message: 'Something went wrong.',
data: state.data,
),
);
}
/// This method is used to store a new quote and its author in the state.
/// It appends the quote to the list of quotes and the author to the list of authors.
///
/// [quote] Single response from the quote
Future<void> storeState(QuoteResponseDTO quote) async {
/// This approach is used to demonstrate persistent data storing that you might use on cases where you need to hold data for a series of screens in a module.
/// For example: If you separate a lengthy registration form in to 5 separate screens, you would need to hold that data until the last step.
state.data.quotes.add(quote);
state.data.authors.add(quote.author);
}
}
24 changes: 1 addition & 23 deletions lib/core/module/home/application/service/cubit/home_state.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,9 @@
part of 'home_cubit.dart';

class HomeState {
const HomeState({
HomeState({
required this.data,
});

final QuoteStateDTO data;
}

class FetchQuoteLoading extends HomeState {
FetchQuoteLoading(QuoteStateDTO data) : super(data: data);
}

class FetchQuoteSuccess extends HomeState {
const FetchQuoteSuccess(QuoteStateDTO data, this.quoteResponse)
: super(data: data);

final QuoteResponseDTO quoteResponse;
}

class FetchQuoteFailed extends HomeState {
const FetchQuoteFailed({
required this.errorCode,
required this.message,
required super.data,
});

final String errorCode;
final String message;
}
18 changes: 11 additions & 7 deletions lib/core/module/home/interfaces/widgets/quotes_card.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'dart:async';

import 'package:flirt/core/application/service/cubit/quote_api_cubit.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand All @@ -17,10 +19,10 @@ class _QuotesCardState extends State<QuotesCard> {
@override
void initState() {
super.initState();
context.read<HomeCubit>().fetchQuote();
context.read<QuoteAPICubit>().fetchQuote();
_quoteTimer = Timer.periodic(
const Duration(seconds: 15),
(_) => context.read<HomeCubit>().fetchQuote(),
(_) => context.read<QuoteAPICubit>().fetchQuote(),
);
}

Expand All @@ -34,12 +36,12 @@ class _QuotesCardState extends State<QuotesCard> {
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;

return BlocConsumer<HomeCubit, HomeState>(
listenWhen: (HomeState previous, HomeState current) =>
return BlocConsumer<QuoteAPICubit, QuoteAPIState>(
listenWhen: (QuoteAPIState previous, QuoteAPIState current) =>
current is FetchQuoteFailed ||
current is FetchQuoteSuccess ||
current is FetchQuoteLoading,
listener: (BuildContext context, HomeState state) {
listener: (BuildContext context, QuoteAPIState state) {
if (state is FetchQuoteFailed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
Expand All @@ -49,13 +51,15 @@ class _QuotesCardState extends State<QuotesCard> {
);
} else if (state is FetchQuoteSuccess) {
setState(() => textOpacity = 1.0);

context.read<HomeCubit>().storeState(state.quoteResponse);
} else if (state is FetchQuoteLoading) {
setState(() => textOpacity = 0);
}
},
buildWhen: (HomeState previous, HomeState current) =>
buildWhen: (QuoteAPIState previous, QuoteAPIState current) =>
current is FetchQuoteSuccess,
builder: (BuildContext context, HomeState state) {
builder: (BuildContext context, QuoteAPIState state) {
if (state is FetchQuoteSuccess) {
return Padding(
padding: const EdgeInsets.only(top: 15, left: 30, right: 30),
Expand Down
8 changes: 6 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flirt/configs/themes.dart';
import 'package:flirt/core/application/service/cubit/quote_api_cubit.dart';
import 'package:flirt/core/infrastructures/repository/quote_repository.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_cubit.dart';
import 'package:flirt/core/module/home/interfaces/screens/home_screen.dart';
Expand All @@ -20,11 +21,14 @@ class App extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<HomeCubit>(
create: (BuildContext context) => HomeCubit(
BlocProvider<QuoteAPICubit>(
create: (BuildContext context) => QuoteAPICubit(
quoteRepository: QuoteRepository(),
),
),
BlocProvider<HomeCubit>(
create: (BuildContext context) => HomeCubit(),
),
],
child: MaterialApp(
home: _HomePageState(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// ignore_for_file: depend_on_referenced_packages

import 'package:bloc_test/bloc_test.dart';
import 'package:flirt/core/application/service/cubit/quote_api_cubit.dart';
import 'package:flirt/core/domain/models/api_error_response.dart';
import 'package:flirt/core/domain/models/api_response.dart';
import 'package:flirt/core/domain/models/quote/quote_response.dart';
import 'package:flirt/core/infrastructures/repository/quote_repository.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_cubit.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

Expand All @@ -18,7 +18,7 @@ void main() {
mockQuoteRepository = MockQuoteRepository();
});
group('Quote cubit.', () {
blocTest<HomeCubit, HomeState>(
blocTest<QuoteAPICubit, QuoteAPIState>(
'On successful fetch quote, it should emit FetchQuoteSuccess.',
build: () {
when(() => mockQuoteRepository.fetchQuote()).thenAnswer((_) async {
Expand All @@ -31,16 +31,16 @@ void main() {
);
});

return HomeCubit(quoteRepository: mockQuoteRepository);
return QuoteAPICubit(quoteRepository: mockQuoteRepository);
},
act: (HomeCubit cubit) => cubit.fetchQuote(),
expect: () => <TypeMatcher<HomeState>>[
act: (QuoteAPICubit cubit) => cubit.fetchQuote(),
expect: () => <TypeMatcher<QuoteAPIState>>[
isA<FetchQuoteLoading>(),
isA<FetchQuoteSuccess>(),
],
);

blocTest<HomeCubit, HomeState>(
blocTest<QuoteAPICubit, QuoteAPIState>(
'On failed fetch quote, it should emit FetchQuoteFailed.',
build: () {
when(() => mockQuoteRepository.fetchQuote()).thenThrow(
Expand All @@ -50,10 +50,10 @@ void main() {
),
);

return HomeCubit(quoteRepository: mockQuoteRepository);
return QuoteAPICubit(quoteRepository: mockQuoteRepository);
},
act: (HomeCubit cubit) => cubit.fetchQuote(),
expect: () => <TypeMatcher<HomeState>>[
act: (QuoteAPICubit cubit) => cubit.fetchQuote(),
expect: () => <TypeMatcher<QuoteAPIState>>[
isA<FetchQuoteLoading>(),
isA<FetchQuoteFailed>(),
],
Expand Down
12 changes: 5 additions & 7 deletions test/widget_test/module/home/screens/home_screen_test.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_cubit.dart';
import 'package:flirt/core/module/home/application/service/cubit/home_dto.dart';
import 'package:flirt/core/application/service/cubit/quote_api_cubit.dart';
import 'package:flirt/core/module/home/interfaces/screens/home_screen.dart';
import 'package:flirt/core/module/home/interfaces/widgets/quotes_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockQuoteCubit extends MockCubit<HomeState> implements HomeCubit {}
class MockQuoteCubit extends MockCubit<QuoteAPIState>
implements QuoteAPICubit {}

void main() {
late MockQuoteCubit mockQuoteCubit;
Expand All @@ -18,7 +18,7 @@ void main() {
});

Future<void> pumpWidget(WidgetTester tester) async => tester.pumpWidget(
BlocProvider<HomeCubit>(
BlocProvider<QuoteAPICubit>(
create: (BuildContext context) => mockQuoteCubit,
child: const MaterialApp(
home: HomeScreen(),
Expand All @@ -28,9 +28,7 @@ void main() {

void listenStub() {
when(() => mockQuoteCubit.state).thenReturn(
HomeState(
data: QuoteStateDTO(authors: <String>[], quotes: <QuoteResponseDTO>[]),
),
QuoteAPIState(),
);
when(() => mockQuoteCubit.fetchQuote()).thenAnswer((_) async {});
}
Expand Down
Loading

0 comments on commit c3d4d1f

Please sign in to comment.