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

Remove persistence of news articles to achieve easy update and delete possibility #661

Merged
merged 2 commits into from
Aug 12, 2024
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
16 changes: 7 additions & 9 deletions lib/news/models/article.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// News-article object
class Article {
/// Primary key of the article
final int _id;
final int id;

/// Body of the article
final String text;
Expand All @@ -16,17 +16,15 @@ class Article {
final int? categoryId;

/// MD5 hash of the article
final String _md5;
final String md5;

const Article(
{required id,
{required this.id,
required this.text,
required this.title,
required this.pubDate,
required this.categoryId,
required md5})
: _id = id,
_md5 = md5;
required this.md5});

/// Returns an article given a [json] representation of an article.
factory Article.fromJson(Map<String, dynamic> json) {
Expand All @@ -42,13 +40,13 @@ class Article {

/// Returns a json representation of the article object calling this method.
Map<String, Object?> toJson() =>
{'id': _id, 'title': title, 'text': text, 'pub_date': pubDate.toString(), 'category_id': categoryId, 'md5': _md5};
{'id': id, 'title': title, 'text': text, 'pub_date': pubDate.toString(), 'category_id': categoryId, 'md5': md5};

@override
int get hashCode => _md5.hashCode;
int get hashCode => md5.hashCode;

@override
bool operator ==(Object other) {
return other is Article && other._md5 == _md5;
return other is Article && other.md5 == md5;
}
}
115 changes: 16 additions & 99 deletions lib/news/services/news.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';

import 'package:flutter/foundation.dart' hide Category;
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:priobike/http.dart';
import 'package:priobike/logging/logger.dart';
import 'package:priobike/main.dart';
Expand All @@ -27,43 +27,30 @@ class News with ChangeNotifier {
List<Article> articles = [];

/// List with all articles that have been read by the user
Set<Article> readArticles = {};
HashSet<Article> readArticles = HashSet();

/// Map with all categories
Map<int, Category> categories = {};

/// Reset the service to its initial state.
Future<void> reset() async {
hasLoaded = false;
articles = [];
readArticles = {};
categories = {};
articles.clear();
readArticles.clear();
categories.clear();
notifyListeners();
}

/// Returns all available articles from the shared preferences or if not stored locally from the backend server.
Future<void> getArticles() async {
// Get articles that are already saved in the shared preferences on the device.
List<Article> localSavedArticles = await _getStoredArticles();

// If there are articles saved already in the shared preferences on the device
// get the lastSyncDate for later usage eg when deciding whether the "Neu"-tag
// should be shown on the article items in the list.
DateTime? newLastSyncDate;
if (localSavedArticles.isNotEmpty) {
newLastSyncDate = localSavedArticles[0].pubDate;
}

final settings = getIt<Settings>();

String baseUrl = settings.city.selectedBackend(false).path;

final newsArticlesUrl = newLastSyncDate == null
? "https://$baseUrl/news-service/news/articles"
: "https://$baseUrl/news-service/news/articles?from=${DateFormat('yyyy-MM-ddTH:mm:ss').format(newLastSyncDate)}Z";
final newsArticlesUrl = "https://$baseUrl/news-service/news/articles";
final newsArticlesEndpoint = Uri.parse(newsArticlesUrl);

List<Article> articlesFromServer = [];
List<Article> newArticles = [];

// Catch the error if there is no connection to the internet.
try {
Expand All @@ -77,7 +64,7 @@ class News with ChangeNotifier {
await json.decode(response.body).forEach(
(element) {
final Article article = Article.fromJson(element);
articlesFromServer.add(article);
newArticles.add(article);
},
);
hadError = false;
Expand All @@ -87,12 +74,10 @@ class News with ChangeNotifier {
hadError = true;
}

articles = [...articlesFromServer, ...localSavedArticles];
articles = newArticles;

await _getCategories();

await _storeArticles();

readArticles = await _getStoredReadArticles();

hasLoaded = true;
Expand All @@ -103,22 +88,13 @@ class News with ChangeNotifier {
Future<void> _getCategories() async {
for (final article in articles) {
if (article.categoryId != null && !categories.containsKey(article.categoryId)) {
await _getCategory(article.categoryId!);
await _fetchCategory(article.categoryId!);
}
}
}

/// Gets single category given the [categoryId] from the shared preferences or if not stored locally from the backend server
Future<void> _getCategory(int categoryId) async {
Category? category = await _getStoredCategory(categoryId);
if (category != null) {
if (!categories.containsKey(categoryId)) {
categories[categoryId] = category;
}
return;
}

// If the category doesn't exist already in the shared preferences get it from backend server.
/// Fetches single category given the [categoryId] from the backend server
Future<void> _fetchCategory(int categoryId) async {
final settings = getIt<Settings>();
final baseUrl = settings.city.selectedBackend(false).path;
final newsCategoryUrl = "https://$baseUrl/news-service/news/category/${categoryId.toString()}";
Expand All @@ -133,76 +109,17 @@ class News with ChangeNotifier {
throw Exception(err);
}

category = Category.fromJson(json.decode(response.body));
final category = Category.fromJson(json.decode(response.body));

if (!categories.containsKey(categoryId)) {
categories[categoryId] = category;
}

await _storeCategory(category);
} catch (e) {
final hint = "Failed to load category: $e";
log.e(hint);
}
}

/// Store all articles in shared preferences.
Future<void> _storeArticles() async {
if (articles.isEmpty) return;
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().city.selectedBackend(false);

final jsonStr = jsonEncode(articles.map((e) => e.toJson()).toList());
await storage.setString("priobike.news.articles.${backend.name}", jsonStr);
}

/// Store category in shared preferences.
Future<void> _storeCategory(Category category) async {
if (articles.isEmpty) return;
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().city.selectedBackend(false);

final String jsonStr = jsonEncode(category.toJson());
await storage.setString("priobike.news.categories.${backend.name}.${category.id}", jsonStr);
}

/// Get all stored articles
Future<List<Article>> _getStoredArticles() async {
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().city.selectedBackend(false);

final storedArticlesStr = storage.getString("priobike.news.articles.${backend.name}");

if (storedArticlesStr == null) {
return [];
}

List<Article> storedArticles = [];
for (final articleMap in jsonDecode(storedArticlesStr)) {
storedArticles.add(Article.fromJson(articleMap));
}

return storedArticles;
}

/// Get stored category for given [categoryId]
Future<Category?> _getStoredCategory(int categoryId) async {
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().city.selectedBackend(false);

final storedCategoryStr = storage.getString("priobike.news.categories.${backend.name}.$categoryId");

if (storedCategoryStr == null) {
return null;
}

return Category.fromJson(jsonDecode(storedCategoryStr));
}

/// Store all read articles in shared preferences.
Future<void> _storeReadArticles() async {
if (readArticles.isEmpty) return;
Expand All @@ -216,18 +133,18 @@ class News with ChangeNotifier {
}

/// Get stored articles that were already read by the user.
Future<Set<Article>> _getStoredReadArticles() async {
Future<HashSet<Article>> _getStoredReadArticles() async {
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().city.selectedBackend(false);

final storedReadArticlesStr = storage.getString("priobike.news.read_articles.${backend.name}");

if (storedReadArticlesStr == null) {
return {};
return HashSet();
}

Set<Article> storedReadArticles = {};
HashSet<Article> storedReadArticles = HashSet();
for (final articleMap in jsonDecode(storedReadArticlesStr)) {
storedReadArticles.add(Article.fromJson(articleMap));
}
Expand Down
Loading