Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ lib/generated_plugin_registrant.dart

# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

# Code coverage directory
coverage/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[![Codemagic build status](https://api.codemagic.io/apps/5e93249b1838ac3d3e52a5bc/5e93249b1838ac3d3e52a5bb/status_badge.svg)](https://codemagic.io/apps/5e93249b1838ac3d3e52a5bc/5e93249b1838ac3d3e52a5bb/latest_build)
[![codecov](https://codecov.io/gh/CoderJava/Flutter-News-App/branch/dev-v2.0.0/graph/badge.svg)](https://codecov.io/gh/CoderJava/Flutter-News-App)
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart)

# Flutter News App
News App developed with Flutter and API from [News API](https://newsapi.org)

Expand Down
35 changes: 35 additions & 0 deletions codemagic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Automatically generated on 2020-04-12 UTC from https://codemagic.io/app/5e93249b1838ac3d3e52a5bc/settings
# Note that this configuration is not an exact match to UI settings. Review and adjust as necessary.

workflows:
default-workflow:
name: Default Workflow
environment:
vars:
CODECOV_TOKEN: Encrypted(Z0FBQUFBQmVrMGRUSGRXRFVweTVGYUIzMThqbGNkNW5aSHE3SnYyeHg4amk3NEF4ZGVwakdXSFBmQVVoRmczZk9GaHdCakhORWcxMUtwZFVYeXJCSHhjWUNiZWM2d2tCUUpfWGwxSjZCaEhfUXlzOS1GN3VRRmdWanlNYnNxTmlleExZak5iWEFLTm0=)
flutter: stable
xcode: latest
cocoapods: default
triggering:
events:
- push
branch_patterns:
- pattern: '*'
include: true
source: true
scripts:
- flutter packages pub get
- |
#!/bin/sh
flutter test --coverage
- flutter test
- |
#!/bin/bash
echo $CODECOV_TOKEN > .cc_token
bash <(curl -s https://codecov.io/bash) -t @.cc_token
artifacts:
- flutter_drive.log
publishing:
email:
recipients:
- kolonel.yudisetiawan@gmail.com
3 changes: 3 additions & 0 deletions lib/core/error/exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class CacheException implements Exception {}

class ConnectionException implements Exception {}
45 changes: 45 additions & 0 deletions lib/core/error/failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {}

final String messageConnectionFailure = 'messageConnectionFailure';

class ServerFailure extends Failure {
final String errorMessage;

ServerFailure(this.errorMessage);

@override
List<Object> get props => [errorMessage];

@override
String toString() {
return 'ServerFailure{errorMessage: $errorMessage}';
}
}

class CacheFailure extends Failure {
final String errorMessage;

CacheFailure(this.errorMessage);

@override
List<Object> get props => [errorMessage];

@override
String toString() {
return 'CacheFailure{errorMessage: $errorMessage}';
}
}

class ConnectionFailure extends Failure {
final String errorMessage = messageConnectionFailure;

@override
List<Object> get props => [errorMessage];

@override
String toString() {
return 'ConnectionFailure{errorMessage: $errorMessage}';
}
}
14 changes: 14 additions & 0 deletions lib/core/network/network_info.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:data_connection_checker/data_connection_checker.dart';

abstract class NetworkInfo {
Future<bool> get isConnected;
}

class NetworkInfoImpl implements NetworkInfo {
final DataConnectionChecker dataConnectionChecker;

NetworkInfoImpl(this.dataConnectionChecker);

@override
Future<bool> get isConnected => dataConnectionChecker.hasConnection;
}
12 changes: 12 additions & 0 deletions lib/core/usecase/usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_news_app/core/error/failure.dart';

abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}

class NoParams extends Equatable {
@override
List<Object> get props => [];
}
51 changes: 51 additions & 0 deletions lib/core/util/dio_logging_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:dio/dio.dart';
import 'package:flutter_news_app/config/flavor_config.dart';

class DioLoggingInterceptor extends InterceptorsWrapper {
@override
Future onRequest(RequestOptions options) async {
if (FlavorConfig.instance.flavor == Flavor.DEVELOPMENT) {
print("--> ${options.method != null ? options.method.toUpperCase() : 'METHOD'} ${"" + (options.baseUrl ?? "") + (options.path ?? "")}");
print('Headers:');
options.headers.forEach((k, v) => print('$k: $v'));
if (options.queryParameters != null) {
print('queryParameters:');
options.queryParameters.forEach((k, v) => print('$k: $v'));
}
if (options.data != null) {
print('Body: ${options.data}');
}
print("--> END ${options.method != null ? options.method.toUpperCase() : 'METHOD'}");
}

// example for add header authorization
/*if (options.headers.containsKey(requiredToken)) {
options.headers.remove(requiredToken);
options.headers.addAll({'Authorization': 'Bearer $token'});
}*/
return options;
}

@override
Future onResponse(Response response) {
if (FlavorConfig.instance.flavor == Flavor.DEVELOPMENT) {
print("<-- ${response.statusCode} ${(response.request != null ? (response.request.baseUrl + response.request.path) : 'URL')}");
print('Headers:');
response.headers?.forEach((k, v) => print('$k: $v'));
print('Response: ${response.data}');
print('<-- END HTTP');
}
return super.onResponse(response);
}

@override
Future onError(DioError dioError) async {
if (FlavorConfig.instance.flavor == Flavor.DEVELOPMENT) {
print(
"<-- ${dioError.message} ${(dioError.response?.request != null ? (dioError.response.request.baseUrl + dioError.response.request.path) : 'URL')}");
print("${dioError.response != null ? dioError.response.data : 'Unknown Error'}");
print('<-- End error');
}
return super.onError(dioError);
}
}
1 change: 1 addition & 0 deletions lib/core/util/util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'dio_logging_interceptor.dart';
28 changes: 28 additions & 0 deletions lib/feature/data/model/categorynews/category_news_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'category_news_model.g.dart';

@JsonSerializable()
class CategoryNewsModel extends Equatable {
final String image;
final String title;

CategoryNewsModel({
@required this.image,
@required this.title,
});

factory CategoryNewsModel.fromJson(Map<String, dynamic> json) => _$CategoryNewsModelFromJson(json);

Map<String, dynamic> toJson() => _$CategoryNewsModelToJson(this);

@override
List<Object> get props => [image, title];

@override
String toString() {
return 'CategoryNewsModel{image: $image, title: $title}';
}
}
20 changes: 20 additions & 0 deletions lib/feature/data/model/categorynews/category_news_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies:
flutter_screenutil: ^1.1.0

# How to get the most value from Dart static analysis.
pedantic: ^1.9.0
pedantic: ^1.8.0+1

# Flutter plugin providing detailed information about the device (make, model, etc), and
# Android or OS version the app is running on.
Expand Down
60 changes: 60 additions & 0 deletions test/core/error/failure_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter_news_app/core/error/failure.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
final tErrorMessage = 'testErrorMessage';

group('ServerFailure', () {
test(
'make sure the props value is [errorMessage]',
() async {
// assert
expect(ServerFailure(tErrorMessage).props, [tErrorMessage]);
},
);

test(
'make sure the value of the toString() function',
() async {
// assert
expect(ServerFailure(tErrorMessage).toString(), 'ServerFailure{errorMessage: $tErrorMessage}');
},
);
});

group('CacheFailure', () {
test(
'make sure the props value is [errorMessage]',
() async {
// arrange
expect(CacheFailure(tErrorMessage).props, [tErrorMessage]);
},
);

test(
'make sure the value of the toString() function',
() async {
// assert
expect(CacheFailure(tErrorMessage).toString(), 'CacheFailure{errorMessage: $tErrorMessage}');
},
);
});

group('ConnectionFailure', () {
test(
'make sure the props value is [errorMessage]',
() async {
// assert
expect(ConnectionFailure().props, [messageConnectionFailure]);
},
);

test(
'make sure the value of the toString() function',
() async {
// assert
expect(ConnectionFailure().toString(), 'ConnectionFailure{errorMessage: $messageConnectionFailure}');
},
);
});
}
50 changes: 50 additions & 0 deletions test/core/network/network_info_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:data_connection_checker/data_connection_checker.dart';
import 'package:flutter_news_app/core/network/network_info.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockDataConnectionChecker extends Mock implements DataConnectionChecker {}

void main() {
NetworkInfoImpl networkInfoImpl;
MockDataConnectionChecker mockDataConnectionChecker;

setUp(() {
mockDataConnectionChecker = MockDataConnectionChecker();
networkInfoImpl = NetworkInfoImpl(mockDataConnectionChecker);
});

group('isConnected', () {
test(
'must call the DataConnectionChecker.hasConnection function and the connection is connected to the internet (online)',
() async {
// arrange
final tHasConnectionFuture = Future.value(true);
when(mockDataConnectionChecker.hasConnection).thenAnswer((_) => tHasConnectionFuture);

// act
final result = networkInfoImpl.isConnected;

// assert
verify(mockDataConnectionChecker.hasConnection);
expect(result, tHasConnectionFuture);
},
);

test(
'must call the DataConnectionChecker.hasConnection function and the connection is not connected to the internet (offline)',
() async {
// arrange
final tHasConnectionFuture = Future.value(false);
when(mockDataConnectionChecker.hasConnection).thenAnswer((_) => tHasConnectionFuture);

// act
final result = networkInfoImpl.isConnected;

// assert
verify(mockDataConnectionChecker.hasConnection);
expect(result, tHasConnectionFuture);
},
);
});
}
12 changes: 12 additions & 0 deletions test/core/usecase/usecase_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:flutter_news_app/core/usecase/usecase.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
test(
'make sure that the props of NoParams is []',
() async {
// assert
expect(NoParams().props, []);
},
);
}
Loading