Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/News Card Component #827

Merged
merged 22 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions lib/core/managers/news_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// FLUTTER / DART / THIRD-PARTIES
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';

// SERVICES
import 'package:notredame/core/services/networking_service.dart';
import 'package:notredame/core/managers/cache_manager.dart';

// MODELS
import 'package:notredame/core/models/news.dart';
import 'package:notredame/core/models/tags.dart';

// UTILS
import 'package:notredame/core/utils/cache_exception.dart';

// OTHER
import 'package:notredame/locator.dart';

/// Repository to access all the news
class NewsRepository {
static const String tag = "NewsRepository";

@visibleForTesting
static const String newsCacheKey = "newsCache";

final Logger _logger = locator<Logger>();

/// Cache manager to access and update the cache.
final CacheManager _cacheManager = locator<CacheManager>();

/// Used to verify if the user has connectivity
final NetworkingService _networkingService = locator<NetworkingService>();

/// List of the news with 3 test news.
List<News> _news = <News>[
News(
id: 1,
title:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tempus arcu sed quam tincidunt, non venenatis orci mollis.",
description: "Test 1 description",
date: DateTime.now(),
image: "https://picsum.photos/400/200",
tags: <Tag>[
Tag(text: "tag1", color: Colors.blue),
Tag(text: "tag2", color: Colors.green),
],
important: true,
),
News(
id: 2,
title: "Test 2",
description: "Test 2 description",
date: DateTime.now(),
image: "https://picsum.photos/400/200",
tags: <Tag>[
Tag(text: "tag1", color: Colors.blue),
Tag(text: "tag2", color: Colors.green),
],
important: false,
),
News(
id: 3,
title: "Test 3",
description: "Test 3 description",
date: DateTime.now(),
image: "https://picsum.photos/400/200",
tags: <Tag>[
Tag(text: "tag1", color: Colors.blue),
Tag(text: "tag2", color: Colors.green),
],
important: true,
),
];

List<News> get news => _news;

/// Get and update the list of news.
/// After fetching the news from the [?] the [CacheManager]
/// is updated with the latest version of the news.
Future<List<News>> getNews({bool fromCacheOnly = false}) async {
// Force fromCacheOnly mode when user has no connectivity
if (!(await _networkingService.hasConnectivity())) {
// ignore: parameter_assignments
fromCacheOnly = true;
}

// Load the news from the cache if the list doesn't exist
if (_news == null) {
await getNewsFromCache();
}

if (fromCacheOnly) {
return _news;
}

final List<News> fetchedNews = fetchNewsFromAPI();

// Update the list of news to avoid duplicate news
for (final News news in fetchedNews) {
if (!_news.contains(news)) {
_news.add(news);
}
}

try {
// Update cache
_cacheManager.update(newsCacheKey, jsonEncode(_news));
} on CacheException catch (_) {
// Do nothing, the caching will retry later and the error has been logged by the [CacheManager]
_logger.e(
"$tag - getNews: exception raised will trying to update the cache.");
}

return _news;
}

Future<void> getNewsFromCache() async {
_news = [];
try {
final List responseCache =
jsonDecode(await _cacheManager.get(newsCacheKey)) as List<dynamic>;

// Build list of news loaded from the cache.
_news = responseCache
.map((e) => News.fromJson(e as Map<String, dynamic>))
.toList();

_logger
.d("$tag - getNewsFromCache: ${_news.length} news loaded from cache");
} on CacheException catch (_) {
_logger.e(
"$tag - getNewsFromCache: exception raised will trying to load news from cache.");
}
}

// TODO : Fetch news from the API
List<News> fetchNewsFromAPI() {
final List<News> fetchedNews = [];

fetchedNews.add(News(
id: 1,
title: "Nouvelle fonctionnalité",
description:
"Vous pouvez désormais consulter les nouvelles de votre école directement dans l'application. Pour cela, cliquez sur le menu en haut à gauche et sélectionnez \"Nouvelles\".",
image: "https://i.imgur.com/1ZQ2Z0M.png",
tags: [
Tag(text: "Nouvelles", color: Colors.blue),
],
date: DateTime.now(),
important: true,
));

// Add three more random news
for (int i = 0; i < 3; i++) {
fetchedNews.add(News(
id: i + 2,
title: "Nouvelle fonctionnalité",
description:
"Vous pouvez désormais consulter les nouvelles de votre école directement dans l'application. Pour cela, cliquez sur le menu en haut à gauche et sélectionnez \"Nouvelles\".",
image: "https://i.imgur.com/1ZQ2Z0M.png",
tags: [
Tag(text: "Nouvelles", color: Colors.blue),
],
date: DateTime.now(),
important: false,
));
}

_logger.d("$tag - getNews: fetched ${fetchedNews.length} news.");

return fetchedNews;
}
}
48 changes: 48 additions & 0 deletions lib/core/models/news.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:notredame/core/models/tags.dart';

class News {
final int id;
final String title;
final String description;
final String image;
final List<Tag> tags;
final DateTime date;
final bool important;

News({
@required this.id,
@required this.title,
@required this.description,
@required this.image,
@required this.tags,
@required this.date,
@required this.important,
});

/// Used to create [News] instance from a JSON file
factory News.fromJson(Map<String, dynamic> map) {
return News(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
image: map['image'] as String,
tags: List<Tag>.from(
(map['tags'] as List).map((tagMap) => Tag.fromJson(tagMap))),
date: DateTime.parse(map['date'] as String),
important: map['important'] as bool,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'image': image,
'tags': tags.map((tag) => tag.toJson()).toList(),
'date': date.toString(),
'important': important,
};
}
}
28 changes: 28 additions & 0 deletions lib/core/models/tags.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';

class Tag {
final String text;
final Color color;

Tag({
@required this.text,
@required this.color,
});

factory Tag.fromJson(dynamic tagMap) {
if (tagMap is Map<String, dynamic>) {
return Tag(
text: tagMap['text'] as String,
color: Color(tagMap['color'] as int),
);
}
throw ArgumentError('Invalid tagMap type. Expected Map<String, dynamic>.');
}

Map<String, dynamic> toJson() {
return {
'text': text,
'color': color.value,
};
}
}
4 changes: 4 additions & 0 deletions lib/core/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ mixin Utils {
: darkColor;
}

static bool isDarkTheme(BuildContext context) {
return Theme.of(context).brightness == Brightness.dark;
}

/// Get first day of the week depending on startingDay which corresponds to weekday
static DateTime getFirstDayOfCurrentWeek(
DateTime currentDate, StartingDayOfWeek startingDay) {
Expand Down
77 changes: 77 additions & 0 deletions lib/core/viewmodels/news_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// FLUTTER / DART / THIRD-PARTIES
import 'package:fluttertoast/fluttertoast.dart';
import 'package:stacked/stacked.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

// CONSTANTS

// MANAGER
import 'package:notredame/core/managers/news_repository.dart';
import 'package:notredame/core/managers/settings_manager.dart';

// MODELS
import 'package:notredame/core/models/news.dart';

// UTILS

// OTHER
import 'package:notredame/locator.dart';

class NewsViewModel extends FutureViewModel<List<News>> {
/// Load the events
final NewsRepository _newsRepository = locator<NewsRepository>();

/// Manage de settings
final SettingsManager _settingsManager = locator<SettingsManager>();

/// Localization class of the application.
final AppIntl _appIntl;

/// News list
List<News> _news = [];

/// Return the list of all the news.
List<News> get news {
_news = [];

// Build the list of news
for (final n in _newsRepository.news) {
_news.add(n);
}

return _news;
}

/// Get current locale
Locale get locale => _settingsManager.locale;

NewsViewModel({@required AppIntl intl}) : _appIntl = intl;

bool isLoadingEvents = false;

@override
Future<List<News>> futureToRun() =>
// ignore: missing_return
_newsRepository.getNews(fromCacheOnly: true).then((value) {
setBusyForObject(isLoadingEvents, true);
_newsRepository
.getNews()
// ignore: return_type_invalid_for_catch_error
.catchError(onError)
.then((value) {
if (value != null) {
// Reload the list of news
news;
}
}).whenComplete(() {
setBusyForObject(isLoadingEvents, false);
});
});

@override
// ignore: type_annotate_public_apis
void onError(error) {
Fluttertoast.showToast(msg: _appIntl.error);
}
}
2 changes: 2 additions & 0 deletions lib/locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:notredame/core/services/launch_url_service.dart';
// MANAGERS
import 'package:notredame/core/managers/user_repository.dart';
import 'package:notredame/core/managers/course_repository.dart';
import 'package:notredame/core/managers/news_repository.dart';
import 'package:notredame/core/managers/cache_manager.dart';
import 'package:notredame/core/managers/settings_manager.dart';

Expand Down Expand Up @@ -47,6 +48,7 @@ void setupLocator() {
// Managers
locator.registerLazySingleton(() => UserRepository());
locator.registerLazySingleton(() => CourseRepository());
locator.registerLazySingleton(() => NewsRepository());
locator.registerLazySingleton(() => CacheManager());
locator.registerLazySingleton(() => SettingsManager());

Expand Down
Loading