From 06ec3495b1bfb3afc47803501bba91e2e0592718 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Mon, 13 Nov 2023 02:04:00 +0100
Subject: [PATCH] Persist API data

---
 android/gradle.properties                     |   4 +-
 assets/translations/en.json                   |   1 +
 assets/translations/fr.json                   |   1 +
 .../metadata/android/en-US/changelogs/29.txt  |   1 +
 .../metadata/android/fr-FR/changelogs/29.txt  |   1 +
 lib/cubit/data_counts_by_day_cubit.dart       |  39 ++++
 lib/cubit/data_counts_by_day_state.dart       |  15 ++
 lib/cubit/data_counts_by_hour_cubit.dart      |  39 ++++
 lib/cubit/data_counts_by_hour_state.dart      |  15 ++
 lib/cubit/data_discoveries_cubit.dart         |  39 ++++
 lib/cubit/data_discoveries_state.dart         |  15 ++
 lib/cubit/data_statistics_global_cubit.dart   |  39 ++++
 lib/cubit/data_statistics_global_state.dart   |  15 ++
 lib/cubit/data_statistics_recent_cubit.dart   |  39 ++++
 lib/cubit/data_statistics_recent_state.dart   |  15 ++
 lib/cubit/data_timeline_cubit.dart            |  39 ++++
 lib/cubit/data_timeline_state.dart            |  15 ++
 lib/cubit/data_top_artists_cubit.dart         |  39 ++++
 lib/cubit/data_top_artists_state.dart         |  15 ++
 lib/models/counts_by_day.dart                 |  12 +-
 lib/models/counts_by_hour.dart                |  12 +-
 lib/models/discoveries.dart                   |  12 +-
 lib/models/statistics_global.dart             |  22 +-
 lib/models/statistics_recent.dart             |  29 ++-
 lib/models/timeline.dart                      |  12 +-
 lib/models/topartists.dart                    |  16 +-
 lib/ui/widgets/app_bar.dart                   |   2 +
 lib/ui/widgets/cards/counts_by_day.dart       |  45 ++--
 lib/ui/widgets/cards/counts_by_hour.dart      |  45 ++--
 lib/ui/widgets/cards/discoveries.dart         |  89 ++++----
 lib/ui/widgets/cards/statistics_global.dart   |  39 ++--
 lib/ui/widgets/cards/statistics_recent.dart   |  48 ++--
 lib/ui/widgets/cards/timeline.dart            |  59 +++--
 lib/ui/widgets/cards/top_artists.dart         |  45 ++--
 lib/ui/widgets/content/statistics_global.dart |  10 +-
 lib/ui/widgets/content/statistics_recent.dart |   6 +-
 lib/ui/widgets/update_data.dart               | 209 ++++++++++++++++++
 pubspec.yaml                                  |   2 +-
 38 files changed, 835 insertions(+), 265 deletions(-)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/29.txt
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/29.txt
 create mode 100644 lib/cubit/data_counts_by_day_cubit.dart
 create mode 100644 lib/cubit/data_counts_by_day_state.dart
 create mode 100644 lib/cubit/data_counts_by_hour_cubit.dart
 create mode 100644 lib/cubit/data_counts_by_hour_state.dart
 create mode 100644 lib/cubit/data_discoveries_cubit.dart
 create mode 100644 lib/cubit/data_discoveries_state.dart
 create mode 100644 lib/cubit/data_statistics_global_cubit.dart
 create mode 100644 lib/cubit/data_statistics_global_state.dart
 create mode 100644 lib/cubit/data_statistics_recent_cubit.dart
 create mode 100644 lib/cubit/data_statistics_recent_state.dart
 create mode 100644 lib/cubit/data_timeline_cubit.dart
 create mode 100644 lib/cubit/data_timeline_state.dart
 create mode 100644 lib/cubit/data_top_artists_cubit.dart
 create mode 100644 lib/cubit/data_top_artists_state.dart
 create mode 100644 lib/ui/widgets/update_data.dart

diff --git a/android/gradle.properties b/android/gradle.properties
index e644338..d965699 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.0.28
-app.versionCode=28
+app.versionName=0.0.29
+app.versionCode=29
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 3f4f688..6b82e28 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -1,5 +1,6 @@
 {
   "app_name": "Scrobbles",
+  "": "",
 
   "bottom_nav_home": "Home",
   "bottom_nav_discoveries": "Discoveries",
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 2f2fb63..84b4236 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -1,5 +1,6 @@
 {
   "app_name": "Scrobbles",
+  "": "",
 
   "bottom_nav_home": "Accueil",
   "bottom_nav_discoveries": "Découvertes",
diff --git a/fastlane/metadata/android/en-US/changelogs/29.txt b/fastlane/metadata/android/en-US/changelogs/29.txt
new file mode 100644
index 0000000..78d9620
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/29.txt
@@ -0,0 +1 @@
+Save scrobbles data from API.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/29.txt b/fastlane/metadata/android/fr-FR/changelogs/29.txt
new file mode 100644
index 0000000..b251952
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/29.txt
@@ -0,0 +1 @@
+Sauvegarde des données d'écoutes de l'API.
diff --git a/lib/cubit/data_counts_by_day_cubit.dart b/lib/cubit/data_counts_by_day_cubit.dart
new file mode 100644
index 0000000..032aec8
--- /dev/null
+++ b/lib/cubit/data_counts_by_day_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/counts_by_day.dart';
+
+part 'data_counts_by_day_state.dart';
+
+class DataCountsByDayCubit extends HydratedCubit<DataCountsByDayState> {
+  DataCountsByDayCubit() : super(const DataCountsByDayState());
+
+  void getData(DataCountsByDayState state) {
+    emit(state);
+  }
+
+  CountsByDayData? getValue() {
+    return state.countsByDay;
+  }
+
+  void setValue(CountsByDayData? countsByDay) {
+    emit(DataCountsByDayState(
+      countsByDay: countsByDay,
+    ));
+  }
+
+  @override
+  DataCountsByDayState? fromJson(Map<String, dynamic> json) {
+    return DataCountsByDayState(
+      countsByDay: CountsByDayData.fromJson(json['countsByDay']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataCountsByDayState state) {
+    return <String, Object?>{
+      'countsByDay': state.countsByDay?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_counts_by_day_state.dart b/lib/cubit/data_counts_by_day_state.dart
new file mode 100644
index 0000000..724bd73
--- /dev/null
+++ b/lib/cubit/data_counts_by_day_state.dart
@@ -0,0 +1,15 @@
+part of 'data_counts_by_day_cubit.dart';
+
+@immutable
+class DataCountsByDayState extends Equatable {
+  const DataCountsByDayState({
+    this.countsByDay,
+  });
+
+  final CountsByDayData? countsByDay;
+
+  @override
+  List<Object?> get props => <Object?>[
+        countsByDay,
+      ];
+}
diff --git a/lib/cubit/data_counts_by_hour_cubit.dart b/lib/cubit/data_counts_by_hour_cubit.dart
new file mode 100644
index 0000000..2184993
--- /dev/null
+++ b/lib/cubit/data_counts_by_hour_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/counts_by_hour.dart';
+
+part 'data_counts_by_hour_state.dart';
+
+class DataCountsByHourCubit extends HydratedCubit<DataCountsByHourState> {
+  DataCountsByHourCubit() : super(const DataCountsByHourState());
+
+  void getData(DataCountsByHourState state) {
+    emit(state);
+  }
+
+  CountsByHourData? getValue() {
+    return state.countsByHour;
+  }
+
+  void setValue(CountsByHourData? countsByHour) {
+    emit(DataCountsByHourState(
+      countsByHour: countsByHour,
+    ));
+  }
+
+  @override
+  DataCountsByHourState? fromJson(Map<String, dynamic> json) {
+    return DataCountsByHourState(
+      countsByHour: CountsByHourData.fromJson(json['countsByHour']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataCountsByHourState state) {
+    return <String, Object?>{
+      'countsByHour': state.countsByHour?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_counts_by_hour_state.dart b/lib/cubit/data_counts_by_hour_state.dart
new file mode 100644
index 0000000..3fc9aef
--- /dev/null
+++ b/lib/cubit/data_counts_by_hour_state.dart
@@ -0,0 +1,15 @@
+part of 'data_counts_by_hour_cubit.dart';
+
+@immutable
+class DataCountsByHourState extends Equatable {
+  const DataCountsByHourState({
+    this.countsByHour,
+  });
+
+  final CountsByHourData? countsByHour;
+
+  @override
+  List<Object?> get props => <Object?>[
+        countsByHour,
+      ];
+}
diff --git a/lib/cubit/data_discoveries_cubit.dart b/lib/cubit/data_discoveries_cubit.dart
new file mode 100644
index 0000000..1b9ff97
--- /dev/null
+++ b/lib/cubit/data_discoveries_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/discoveries.dart';
+
+part 'data_discoveries_state.dart';
+
+class DataDiscoveriesCubit extends HydratedCubit<DataDiscoveriesState> {
+  DataDiscoveriesCubit() : super(const DataDiscoveriesState());
+
+  void getData(DataDiscoveriesState state) {
+    emit(state);
+  }
+
+  DiscoveriesData? getValue() {
+    return state.discoveries;
+  }
+
+  void setValue(DiscoveriesData? discoveries) {
+    emit(DataDiscoveriesState(
+      discoveries: discoveries,
+    ));
+  }
+
+  @override
+  DataDiscoveriesState? fromJson(Map<String, dynamic> json) {
+    return DataDiscoveriesState(
+      discoveries: DiscoveriesData.fromJson(json['discoveries']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataDiscoveriesState state) {
+    return <String, Object?>{
+      'discoveries': state.discoveries?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_discoveries_state.dart b/lib/cubit/data_discoveries_state.dart
new file mode 100644
index 0000000..1d43883
--- /dev/null
+++ b/lib/cubit/data_discoveries_state.dart
@@ -0,0 +1,15 @@
+part of 'data_discoveries_cubit.dart';
+
+@immutable
+class DataDiscoveriesState extends Equatable {
+  const DataDiscoveriesState({
+    this.discoveries,
+  });
+
+  final DiscoveriesData? discoveries;
+
+  @override
+  List<Object?> get props => <Object?>[
+        discoveries,
+      ];
+}
diff --git a/lib/cubit/data_statistics_global_cubit.dart b/lib/cubit/data_statistics_global_cubit.dart
new file mode 100644
index 0000000..28e7e6b
--- /dev/null
+++ b/lib/cubit/data_statistics_global_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/statistics_global.dart';
+
+part 'data_statistics_global_state.dart';
+
+class DataStatisticsGlobalCubit extends HydratedCubit<DataStatisticsGlobalState> {
+  DataStatisticsGlobalCubit() : super(const DataStatisticsGlobalState());
+
+  void getData(DataStatisticsGlobalState state) {
+    emit(state);
+  }
+
+  StatisticsGlobalData? getValue() {
+    return state.statisticsGlobal;
+  }
+
+  void setValue(StatisticsGlobalData? statisticsGlobal) {
+    emit(DataStatisticsGlobalState(
+      statisticsGlobal: statisticsGlobal,
+    ));
+  }
+
+  @override
+  DataStatisticsGlobalState? fromJson(Map<String, dynamic> json) {
+    return DataStatisticsGlobalState(
+      statisticsGlobal: StatisticsGlobalData.fromJson(json['statisticsGlobal']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataStatisticsGlobalState state) {
+    return <String, Object?>{
+      'statisticsGlobal': state.statisticsGlobal?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_statistics_global_state.dart b/lib/cubit/data_statistics_global_state.dart
new file mode 100644
index 0000000..89ef0fd
--- /dev/null
+++ b/lib/cubit/data_statistics_global_state.dart
@@ -0,0 +1,15 @@
+part of 'data_statistics_global_cubit.dart';
+
+@immutable
+class DataStatisticsGlobalState extends Equatable {
+  const DataStatisticsGlobalState({
+    this.statisticsGlobal,
+  });
+
+  final StatisticsGlobalData? statisticsGlobal;
+
+  @override
+  List<Object?> get props => <Object?>[
+        statisticsGlobal,
+      ];
+}
diff --git a/lib/cubit/data_statistics_recent_cubit.dart b/lib/cubit/data_statistics_recent_cubit.dart
new file mode 100644
index 0000000..d2bc445
--- /dev/null
+++ b/lib/cubit/data_statistics_recent_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/statistics_recent.dart';
+
+part 'data_statistics_recent_state.dart';
+
+class DataStatisticsRecentCubit extends HydratedCubit<DataStatisticsRecentState> {
+  DataStatisticsRecentCubit() : super(const DataStatisticsRecentState());
+
+  void getData(DataStatisticsRecentState state) {
+    emit(state);
+  }
+
+  StatisticsRecentData? getValue() {
+    return state.statisticsRecent;
+  }
+
+  void setValue(StatisticsRecentData? statisticsRecent) {
+    emit(DataStatisticsRecentState(
+      statisticsRecent: statisticsRecent,
+    ));
+  }
+
+  @override
+  DataStatisticsRecentState? fromJson(Map<String, dynamic> json) {
+    return DataStatisticsRecentState(
+      statisticsRecent: StatisticsRecentData.fromJson(json['statisticsRecent']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataStatisticsRecentState state) {
+    return <String, Object?>{
+      'statisticsRecent': state.statisticsRecent?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_statistics_recent_state.dart b/lib/cubit/data_statistics_recent_state.dart
new file mode 100644
index 0000000..1bf9d2f
--- /dev/null
+++ b/lib/cubit/data_statistics_recent_state.dart
@@ -0,0 +1,15 @@
+part of 'data_statistics_recent_cubit.dart';
+
+@immutable
+class DataStatisticsRecentState extends Equatable {
+  const DataStatisticsRecentState({
+    this.statisticsRecent,
+  });
+
+  final StatisticsRecentData? statisticsRecent;
+
+  @override
+  List<Object?> get props => <Object?>[
+        statisticsRecent,
+      ];
+}
diff --git a/lib/cubit/data_timeline_cubit.dart b/lib/cubit/data_timeline_cubit.dart
new file mode 100644
index 0000000..597e594
--- /dev/null
+++ b/lib/cubit/data_timeline_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/timeline.dart';
+
+part 'data_timeline_state.dart';
+
+class DataTimelineCubit extends HydratedCubit<DataTimelineState> {
+  DataTimelineCubit() : super(const DataTimelineState());
+
+  void getData(DataTimelineState state) {
+    emit(state);
+  }
+
+  TimelineData? getValue() {
+    return state.timeline;
+  }
+
+  void setValue(TimelineData? timeline) {
+    emit(DataTimelineState(
+      timeline: timeline,
+    ));
+  }
+
+  @override
+  DataTimelineState? fromJson(Map<String, dynamic> json) {
+    return DataTimelineState(
+      timeline: TimelineData.fromJson(json['timeline']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataTimelineState state) {
+    return <String, Object?>{
+      'timeline': state.timeline?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_timeline_state.dart b/lib/cubit/data_timeline_state.dart
new file mode 100644
index 0000000..b0531c7
--- /dev/null
+++ b/lib/cubit/data_timeline_state.dart
@@ -0,0 +1,15 @@
+part of 'data_timeline_cubit.dart';
+
+@immutable
+class DataTimelineState extends Equatable {
+  const DataTimelineState({
+    this.timeline,
+  });
+
+  final TimelineData? timeline;
+
+  @override
+  List<Object?> get props => <Object?>[
+        timeline,
+      ];
+}
diff --git a/lib/cubit/data_top_artists_cubit.dart b/lib/cubit/data_top_artists_cubit.dart
new file mode 100644
index 0000000..0011c5e
--- /dev/null
+++ b/lib/cubit/data_top_artists_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/topartists.dart';
+
+part 'data_top_artists_state.dart';
+
+class DataTopArtistsCubit extends HydratedCubit<DataTopArtistsState> {
+  DataTopArtistsCubit() : super(const DataTopArtistsState());
+
+  void getData(DataTopArtistsState state) {
+    emit(state);
+  }
+
+  TopArtistsData? getValue(key) {
+    return state.topArtists;
+  }
+
+  void setValue(TopArtistsData? topArtists) {
+    emit(DataTopArtistsState(
+      topArtists: topArtists,
+    ));
+  }
+
+  @override
+  DataTopArtistsState? fromJson(Map<String, dynamic> json) {
+    return DataTopArtistsState(
+      topArtists: TopArtistsData.fromJson(json['topArtists']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataTopArtistsState state) {
+    return <String, Object?>{
+      'topArtists': state.topArtists?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_top_artists_state.dart b/lib/cubit/data_top_artists_state.dart
new file mode 100644
index 0000000..e6d14c0
--- /dev/null
+++ b/lib/cubit/data_top_artists_state.dart
@@ -0,0 +1,15 @@
+part of 'data_top_artists_cubit.dart';
+
+@immutable
+class DataTopArtistsState extends Equatable {
+  const DataTopArtistsState({
+    this.topArtists,
+  });
+
+  final TopArtistsData? topArtists;
+
+  @override
+  List<Object?> get props => <Object?>[
+        topArtists,
+      ];
+}
diff --git a/lib/models/counts_by_day.dart b/lib/models/counts_by_day.dart
index fcb4551..27e41f6 100644
--- a/lib/models/counts_by_day.dart
+++ b/lib/models/counts_by_day.dart
@@ -19,11 +19,7 @@ class CountsByDayData {
     return CountsByDayData(data: data);
   }
 
-  factory CountsByDayData.createEmpty() {
-    return CountsByDayData.fromJson({'counts-by-day': {}});
-  }
-
-  String toString() {
+  Map<String, Object?>? toJson() {
     Map<String, double> map = {};
 
     this.data.keys.forEach((day) {
@@ -31,6 +27,10 @@ class CountsByDayData {
       map[day.toString()] = value != null ? value.toDouble() : 0.0;
     });
 
-    return jsonEncode({'counts-by-day': map});
+    return {'counts-by-day': map};
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/counts_by_hour.dart b/lib/models/counts_by_hour.dart
index f0c6d50..7bf3802 100644
--- a/lib/models/counts_by_hour.dart
+++ b/lib/models/counts_by_hour.dart
@@ -21,11 +21,7 @@ class CountsByHourData {
     return CountsByHourData(data: data);
   }
 
-  factory CountsByHourData.createEmpty() {
-    return CountsByHourData.fromJson({});
-  }
-
-  String toString() {
+  Map<String, Object?>? toJson() {
     Map<String, double> map = {};
 
     this.data.keys.forEach((day) {
@@ -33,6 +29,10 @@ class CountsByHourData {
       map[day.toString()] = value != null ? value.toDouble() : 0.0;
     });
 
-    return jsonEncode({'counts-by-hour': map});
+    return {'counts-by-hour': map};
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/discoveries.dart b/lib/models/discoveries.dart
index 14f5819..386098f 100644
--- a/lib/models/discoveries.dart
+++ b/lib/models/discoveries.dart
@@ -36,11 +36,7 @@ class DiscoveriesData {
     return DiscoveriesData(data: data);
   }
 
-  factory DiscoveriesData.createEmpty() {
-    return DiscoveriesData.fromJson({});
-  }
-
-  String toString() {
+  Map<String, Object?>? toJson() {
     Map<String, Map<String, int>> map = {};
 
     this.data.keys.forEach((element) {
@@ -51,6 +47,10 @@ class DiscoveriesData {
       };
     });
 
-    return jsonEncode(map);
+    return map;
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/statistics_global.dart b/lib/models/statistics_global.dart
index 4eb90fc..1f87265 100644
--- a/lib/models/statistics_global.dart
+++ b/lib/models/statistics_global.dart
@@ -1,8 +1,8 @@
 import 'dart:convert';
 
 class StatisticsGlobalData {
-  final int totalCount;
-  final DateTime lastScrobble;
+  final int? totalCount;
+  final DateTime? lastScrobble;
 
   const StatisticsGlobalData({
     required this.totalCount,
@@ -11,27 +11,25 @@ class StatisticsGlobalData {
 
   factory StatisticsGlobalData.fromJson(Map<String, dynamic>? json) {
     return StatisticsGlobalData(
-      totalCount: (json?['totalCount'] != null) ? (json?['totalCount'] as int) : 0,
+      totalCount: (json?['totalCount'] != null) ? (json?['totalCount'] as int) : null,
       lastScrobble: (json?['lastScrobble'] != null && json?['lastScrobble']['date'] != null)
           ? DateTime.parse(
               json?['lastScrobble']['date'],
             )
-          : DateTime.now(),
+          : null,
     );
   }
 
-  factory StatisticsGlobalData.createEmpty() {
-    return StatisticsGlobalData.fromJson({});
-  }
-
-  String toString() {
-    Map<String, dynamic> map = {
+  Map<String, Object?>? toJson() {
+    return <String, Object?>{
       'totalCount': this.totalCount,
       'lastScrobble': {
-        'date': this.lastScrobble.toString(),
+        'date': this.lastScrobble != null ? this.lastScrobble.toString() : null,
       },
     };
+  }
 
-    return jsonEncode(map);
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/statistics_recent.dart b/lib/models/statistics_recent.dart
index d72173f..1f342f9 100644
--- a/lib/models/statistics_recent.dart
+++ b/lib/models/statistics_recent.dart
@@ -1,10 +1,10 @@
 import 'dart:convert';
 
 class StatisticsRecentData {
-  final int recentCount;
-  final int firstPlayedArtistsCount;
-  final int firstPlayedTracksCount;
-  final int selectedPeriod;
+  final int? recentCount;
+  final int? firstPlayedArtistsCount;
+  final int? firstPlayedTracksCount;
+  final int? selectedPeriod;
 
   const StatisticsRecentData({
     required this.recentCount,
@@ -15,29 +15,28 @@ class StatisticsRecentData {
 
   factory StatisticsRecentData.fromJson(Map<String, dynamic>? json) {
     return StatisticsRecentData(
-      recentCount: (json?['recentCount'] != null) ? (json?['recentCount'] as int) : 0,
+      recentCount: (json?['recentCount'] != null) ? (json?['recentCount'] as int) : null,
       firstPlayedArtistsCount: (json?['firstPlayedArtistsCount'] != null)
           ? (json?['firstPlayedArtistsCount'] as int)
-          : 0,
+          : null,
       firstPlayedTracksCount: (json?['firstPlayedTracksCount'] != null)
           ? (json?['firstPlayedTracksCount'] as int)
-          : 0,
-      selectedPeriod: (json?['selectedPeriod'] != null) ? (json?['selectedPeriod'] as int) : 0,
+          : null,
+      selectedPeriod:
+          (json?['selectedPeriod'] != null) ? (json?['selectedPeriod'] as int) : null,
     );
   }
 
-  factory StatisticsRecentData.createEmpty() {
-    return StatisticsRecentData.fromJson({});
-  }
-
-  String toString() {
-    Map<String, dynamic> map = {
+  Map<String, Object?>? toJson() {
+    return <String, Object?>{
       'recentCount': this.recentCount,
       'firstPlayedArtistsCount': this.firstPlayedArtistsCount,
       'firstPlayedTracksCount': this.firstPlayedTracksCount,
       'selectedPeriod': this.selectedPeriod,
     };
+  }
 
-    return jsonEncode(map);
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/timeline.dart b/lib/models/timeline.dart
index b52a546..37f987b 100644
--- a/lib/models/timeline.dart
+++ b/lib/models/timeline.dart
@@ -36,11 +36,7 @@ class TimelineData {
     return TimelineData(data: data);
   }
 
-  factory TimelineData.createEmpty() {
-    return TimelineData.fromJson({});
-  }
-
-  String toString() {
+  Map<String, Object?>? toJson() {
     Map<String, Map<String, int>> map = {};
 
     this.data.keys.forEach((element) {
@@ -51,6 +47,10 @@ class TimelineData {
       };
     });
 
-    return jsonEncode(map);
+    return map;
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/models/topartists.dart b/lib/models/topartists.dart
index 9a7b418..6c27190 100644
--- a/lib/models/topartists.dart
+++ b/lib/models/topartists.dart
@@ -38,13 +38,7 @@ class TopArtistsData {
     );
   }
 
-  factory TopArtistsData.createEmpty() {
-    return TopArtistsData.fromJson({
-      'top-artists': [],
-    });
-  }
-
-  String toString() {
+  Map<String, Object?>? toJson() {
     List<Map<String, Object>> listArtists = [];
 
     this.topArtists.forEach((TopArtistsDataValue? item) {
@@ -54,8 +48,12 @@ class TopArtistsData {
       });
     });
 
-    return jsonEncode({
+    return {
       'top-artists': listArtists,
-    });
+    };
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
   }
 }
diff --git a/lib/ui/widgets/app_bar.dart b/lib/ui/widgets/app_bar.dart
index 6db981e..63256fb 100644
--- a/lib/ui/widgets/app_bar.dart
+++ b/lib/ui/widgets/app_bar.dart
@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:unicons/unicons.dart';
 
+import 'package:scrobbles/ui/widgets/update_data.dart';
 import 'package:scrobbles/ui/widgets/header_app.dart';
 
 class StandardAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -13,6 +14,7 @@ class StandardAppBar extends StatelessWidget implements PreferredSizeWidget {
     return AppBar(
       title: const AppHeader(text: 'app_name'),
       actions: [
+        UpdateData(),
         IconButton(
           onPressed: () {
             this.notifyParent();
diff --git a/lib/ui/widgets/cards/counts_by_day.dart b/lib/ui/widgets/cards/counts_by_day.dart
index 59d80cb..98c6cfe 100644
--- a/lib/ui/widgets/cards/counts_by_day.dart
+++ b/lib/ui/widgets/cards/counts_by_day.dart
@@ -2,12 +2,12 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
 import 'package:scrobbles/models/counts_by_day.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/charts/counts_by_day.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardCountsByDay extends StatelessWidget {
   const CardCountsByDay({super.key});
@@ -15,30 +15,25 @@ class CardCountsByDay extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final int daysCount = 21;
-    late Future<CountsByDayData> future = ScrobblesApi.fetchCountsByDay(daysCount);
 
-    return FutureBuilder<CountsByDayData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.surface,
-          title: 'counts_by_day'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: ChartCountsByDay(
-            chartData: snapshot.hasData
-                ? CountsByDayData.fromJson(jsonDecode(snapshot.data.toString()))
-                : CountsByDayData.createEmpty(),
-            isLoading: !snapshot.hasData,
-          ),
-        );
-      },
+    return BlocProvider<DataCountsByDayCubit>(
+      create: (BuildContext context) => DataCountsByDayCubit(),
+      child: BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
+        builder: (BuildContext context, DataCountsByDayState state) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.surface,
+            title: 'counts_by_day'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: ChartCountsByDay(
+              chartData: CountsByDayData.fromJson(jsonDecode(state.countsByDay.toString())),
+              isLoading: false,
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/counts_by_hour.dart b/lib/ui/widgets/cards/counts_by_hour.dart
index 6c0d91f..595818e 100644
--- a/lib/ui/widgets/cards/counts_by_hour.dart
+++ b/lib/ui/widgets/cards/counts_by_hour.dart
@@ -2,12 +2,12 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
 import 'package:scrobbles/models/counts_by_hour.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/charts/counts_by_hour.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardCountsByHour extends StatelessWidget {
   const CardCountsByHour({super.key});
@@ -15,30 +15,25 @@ class CardCountsByHour extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final int daysCount = 21;
-    late Future<CountsByHourData> future = ScrobblesApi.fetchCountsByHour(daysCount);
 
-    return FutureBuilder<CountsByHourData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.surface,
-          title: 'counts_by_hour'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: ChartCountsByHour(
-            chartData: snapshot.hasData
-                ? CountsByHourData.fromJson(jsonDecode(snapshot.data.toString()))
-                : CountsByHourData.createEmpty(),
-            isLoading: !snapshot.hasData,
-          ),
-        );
-      },
+    return BlocProvider<DataCountsByHourCubit>(
+      create: (BuildContext context) => DataCountsByHourCubit(),
+      child: BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
+        builder: (BuildContext context, DataCountsByHourState state) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.surface,
+            title: 'counts_by_hour'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: ChartCountsByHour(
+              chartData: CountsByHourData.fromJson(jsonDecode(state.countsByHour.toString())),
+              isLoading: false,
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/discoveries.dart b/lib/ui/widgets/cards/discoveries.dart
index f0cd96d..fb3baf5 100644
--- a/lib/ui/widgets/cards/discoveries.dart
+++ b/lib/ui/widgets/cards/discoveries.dart
@@ -2,13 +2,13 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
 import 'package:scrobbles/models/discoveries.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/charts/discoveries_artists.dart';
 import 'package:scrobbles/ui/widgets/charts/discoveries_tracks.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardDiscoveries extends StatelessWidget {
   const CardDiscoveries({super.key});
@@ -16,51 +16,50 @@ class CardDiscoveries extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final int daysCount = 14;
-    late Future<DiscoveriesData> future = ScrobblesApi.fetchDiscoveries(daysCount);
 
-    return FutureBuilder<DiscoveriesData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
+    return BlocProvider<DataDiscoveriesCubit>(
+      create: (BuildContext context) => DataDiscoveriesCubit(),
+      child: BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
+        builder: (BuildContext context, DataDiscoveriesState state) {
+          final TextTheme textTheme = Theme.of(context).primaryTextTheme;
 
-        final TextTheme textTheme = Theme.of(context).primaryTextTheme;
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.surface,
-          title: 'discoveries_title'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: Column(
-            mainAxisSize: MainAxisSize.min,
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Text(
-                'discoveries_artists_title',
-                style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
-              ).tr(),
-              const SizedBox(height: 8),
-              ChartDiscoveriesArtists(
-                chartData: DiscoveriesData.fromJson(jsonDecode(snapshot.data.toString())),
-                isLoading: !snapshot.hasData,
-              ),
-              const SizedBox(height: 8),
-              Text(
-                'discoveries_tracks_title',
-                style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
-              ).tr(),
-              const SizedBox(height: 8),
-              ChartDiscoveriesTracks(
-                chartData: DiscoveriesData.fromJson(jsonDecode(snapshot.data.toString())),
-                isLoading: !snapshot.hasData,
-              ),
-            ],
-          ),
-        );
-      },
+          return CardContent(
+            color: Theme.of(context).colorScheme.surface,
+            title: 'discoveries_title'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: Column(
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  'discoveries_artists_title',
+                  style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
+                ).tr(),
+                const SizedBox(height: 8),
+                ChartDiscoveriesArtists(
+                  chartData:
+                      DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
+                  isLoading: false,
+                ),
+                const SizedBox(height: 8),
+                Text(
+                  'discoveries_tracks_title',
+                  style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
+                ).tr(),
+                const SizedBox(height: 8),
+                ChartDiscoveriesTracks(
+                  chartData:
+                      DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
+                  isLoading: false,
+                ),
+              ],
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/statistics_global.dart b/lib/ui/widgets/cards/statistics_global.dart
index 0d8feb8..4980d5c 100644
--- a/lib/ui/widgets/cards/statistics_global.dart
+++ b/lib/ui/widgets/cards/statistics_global.dart
@@ -2,38 +2,33 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
 import 'package:scrobbles/models/statistics_global.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/content/statistics_global.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardStatisticsGlobal extends StatelessWidget {
   const CardStatisticsGlobal({super.key});
 
   @override
   Widget build(BuildContext context) {
-    late Future<StatisticsGlobalData> future = ScrobblesApi.fetchGlobalStatistics();
-
-    return FutureBuilder<StatisticsGlobalData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.primary,
-          title: 'global_statistics'.tr(),
-          content: ContentStatisticsGlobal(
-            statistics: snapshot.hasData
-                ? StatisticsGlobalData.fromJson(jsonDecode(snapshot.data.toString()))
-                : StatisticsGlobalData.createEmpty(),
-            isLoading: !snapshot.hasData,
-          ),
-        );
-      },
+    return BlocProvider<DataStatisticsGlobalCubit>(
+      create: (BuildContext context) => DataStatisticsGlobalCubit(),
+      child: BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
+        builder: (BuildContext context, DataStatisticsGlobalState state) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.primary,
+            title: 'global_statistics'.tr(),
+            content: ContentStatisticsGlobal(
+              statistics:
+                  StatisticsGlobalData.fromJson(jsonDecode(state.statisticsGlobal.toString())),
+              isLoading: false,
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/statistics_recent.dart b/lib/ui/widgets/cards/statistics_recent.dart
index 0925c42..9a14c8a 100644
--- a/lib/ui/widgets/cards/statistics_recent.dart
+++ b/lib/ui/widgets/cards/statistics_recent.dart
@@ -2,12 +2,12 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
 import 'package:scrobbles/models/statistics_recent.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/content/statistics_recent.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardStatisticsRecent extends StatelessWidget {
   const CardStatisticsRecent({super.key});
@@ -16,30 +16,26 @@ class CardStatisticsRecent extends StatelessWidget {
   Widget build(BuildContext context) {
     final int daysCount = 21;
 
-    late Future<StatisticsRecentData> future = ScrobblesApi.fetchRecentStatistics(daysCount);
-
-    return FutureBuilder<StatisticsRecentData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.primary,
-          title: 'recent_statistics'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: ContentStatisticsRecent(
-            statistics: snapshot.hasData
-                ? StatisticsRecentData.fromJson(jsonDecode(snapshot.data.toString()))
-                : StatisticsRecentData.createEmpty(),
-            isLoading: !snapshot.hasData,
-          ),
-        );
-      },
+    // data context
+    return BlocProvider<DataStatisticsRecentCubit>(
+      create: (BuildContext context) => DataStatisticsRecentCubit(),
+      child: BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
+        builder: (BuildContext context, DataStatisticsRecentState dataState) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.primary,
+            title: 'recent_statistics'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: ContentStatisticsRecent(
+              statistics: StatisticsRecentData.fromJson(
+                  jsonDecode(dataState.statisticsRecent.toString())),
+              isLoading: false,
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/timeline.dart b/lib/ui/widgets/cards/timeline.dart
index a7ea95d..a7b20d4 100644
--- a/lib/ui/widgets/cards/timeline.dart
+++ b/lib/ui/widgets/cards/timeline.dart
@@ -2,13 +2,13 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_timeline_cubit.dart';
 import 'package:scrobbles/models/timeline.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/charts/timeline_counts.dart';
 import 'package:scrobbles/ui/widgets/charts/timeline_eclecticism.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardTimeline extends StatelessWidget {
   const CardTimeline({super.key});
@@ -16,36 +16,33 @@ class CardTimeline extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final int daysCount = 14;
-    late Future<TimelineData> future = ScrobblesApi.fetchTimeline(daysCount);
 
-    return FutureBuilder<TimelineData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.surface,
-          title: 'timeline_title'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: Stack(
-            children: [
-              ChartTimelineCounts(
-                chartData: TimelineData.fromJson(jsonDecode(snapshot.data.toString())),
-                isLoading: !snapshot.hasData,
-              ),
-              ChartTimelineEclecticism(
-                chartData: TimelineData.fromJson(jsonDecode(snapshot.data.toString())),
-                isLoading: !snapshot.hasData,
-              ),
-            ],
-          ),
-        );
-      },
+    return BlocProvider<DataTimelineCubit>(
+      create: (BuildContext context) => DataTimelineCubit(),
+      child: BlocBuilder<DataTimelineCubit, DataTimelineState>(
+        builder: (BuildContext context, DataTimelineState state) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.surface,
+            title: 'timeline_title'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: Stack(
+              children: [
+                ChartTimelineCounts(
+                  chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
+                  isLoading: false,
+                ),
+                ChartTimelineEclecticism(
+                  chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
+                  isLoading: false,
+                ),
+              ],
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/cards/top_artists.dart b/lib/ui/widgets/cards/top_artists.dart
index d589518..7e05208 100644
--- a/lib/ui/widgets/cards/top_artists.dart
+++ b/lib/ui/widgets/cards/top_artists.dart
@@ -2,12 +2,12 @@ import 'dart:convert';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'package:scrobbles/cubit/data_top_artists_cubit.dart';
 import 'package:scrobbles/models/topartists.dart';
-import 'package:scrobbles/network/scrobbles.dart';
 import 'package:scrobbles/ui/widgets/card_content.dart';
 import 'package:scrobbles/ui/widgets/charts/top_artists.dart';
-import 'package:scrobbles/ui/widgets/error.dart';
 
 class CardTopArtists extends StatelessWidget {
   const CardTopArtists({super.key});
@@ -15,30 +15,25 @@ class CardTopArtists extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final int daysCount = 14;
-    late Future<TopArtistsData> future = ScrobblesApi.fetchTopArtists(daysCount);
 
-    return FutureBuilder<TopArtistsData>(
-      future: future,
-      builder: (context, snapshot) {
-        if (snapshot.hasError) {
-          return ShowErrorWidget(message: '${snapshot.error}');
-        }
-
-        return CardContent(
-          color: Theme.of(context).colorScheme.surface,
-          title: 'top_artists_title'.tr(
-            namedArgs: {
-              'daysCount': daysCount.toString(),
-            },
-          ),
-          content: ChartTopArtists(
-            chartData: snapshot.hasData
-                ? TopArtistsData.fromJson(jsonDecode(snapshot.data.toString()))
-                : TopArtistsData.createEmpty(),
-            isLoading: !snapshot.hasData,
-          ),
-        );
-      },
+    return BlocProvider<DataTopArtistsCubit>(
+      create: (BuildContext context) => DataTopArtistsCubit(),
+      child: BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
+        builder: (BuildContext context, DataTopArtistsState state) {
+          return CardContent(
+            color: Theme.of(context).colorScheme.surface,
+            title: 'top_artists_title'.tr(
+              namedArgs: {
+                'daysCount': daysCount.toString(),
+              },
+            ),
+            content: ChartTopArtists(
+              chartData: TopArtistsData.fromJson(jsonDecode(state.topArtists.toString())),
+              isLoading: false,
+            ),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/lib/ui/widgets/content/statistics_global.dart b/lib/ui/widgets/content/statistics_global.dart
index d171441..c68d249 100644
--- a/lib/ui/widgets/content/statistics_global.dart
+++ b/lib/ui/widgets/content/statistics_global.dart
@@ -24,7 +24,11 @@ class ContentStatisticsGlobal extends StatelessWidget {
           style: textTheme.bodyMedium,
         ).tr(
           namedArgs: {
-            'count': this.isLoading ? placeholder : this.statistics.totalCount.toString(),
+            'count': this.isLoading
+                ? placeholder
+                : (this.statistics.totalCount != null
+                    ? this.statistics.totalCount.toString()
+                    : ''),
           },
         ),
         Text(
@@ -34,7 +38,9 @@ class ContentStatisticsGlobal extends StatelessWidget {
           namedArgs: {
             'datetime': this.isLoading
                 ? placeholder
-                : DateFormat().format(this.statistics.lastScrobble),
+                : (this.statistics.lastScrobble != null
+                    ? DateFormat().format(this.statistics.lastScrobble ?? DateTime.now())
+                    : ''),
           },
         ),
       ],
diff --git a/lib/ui/widgets/content/statistics_recent.dart b/lib/ui/widgets/content/statistics_recent.dart
index 01f04ac..cecbff9 100644
--- a/lib/ui/widgets/content/statistics_recent.dart
+++ b/lib/ui/widgets/content/statistics_recent.dart
@@ -20,7 +20,11 @@ class ContentStatisticsRecent extends StatelessWidget {
       crossAxisAlignment: CrossAxisAlignment.start,
       children: <Widget>[
         Text(
-          'statistics_recent_scrobbles_count_and_discoveries',
+          (this.statistics.recentCount != null &&
+                  this.statistics.firstPlayedArtistsCount != null &&
+                  this.statistics.firstPlayedTracksCount != null)
+              ? 'statistics_recent_scrobbles_count_and_discoveries'
+              : '',
           style: textTheme.bodyMedium,
         ).tr(
           namedArgs: {
diff --git a/lib/ui/widgets/update_data.dart b/lib/ui/widgets/update_data.dart
new file mode 100644
index 0000000..7bc4101
--- /dev/null
+++ b/lib/ui/widgets/update_data.dart
@@ -0,0 +1,209 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
+import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
+import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
+import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
+import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
+import 'package:scrobbles/cubit/data_timeline_cubit.dart';
+import 'package:scrobbles/cubit/data_top_artists_cubit.dart';
+import 'package:scrobbles/models/counts_by_day.dart';
+import 'package:scrobbles/models/counts_by_hour.dart';
+import 'package:scrobbles/models/discoveries.dart';
+import 'package:scrobbles/models/statistics_global.dart';
+import 'package:scrobbles/models/statistics_recent.dart';
+import 'package:scrobbles/models/timeline.dart';
+import 'package:scrobbles/models/topartists.dart';
+import 'package:scrobbles/network/scrobbles.dart';
+import 'package:scrobbles/ui/widgets/error.dart';
+
+class UpdateData extends StatelessWidget {
+  const UpdateData({super.key});
+
+  final int daysCount = 21;
+  final Widget loading = const Text('⏳');
+  final Widget done = const Text('');
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: EdgeInsets.all(4),
+      child: Stack(
+        children: [
+          updateCountsByDay(daysCount),
+          updateCountsByHour(daysCount),
+          updateDiscoveries(daysCount),
+          updateStatisticsGlobal(),
+          updateStatisticsRecent(daysCount),
+          updateTimeline(daysCount),
+          updateTopArtists(daysCount),
+        ],
+      ),
+    );
+  }
+
+  Widget updateCountsByDay(int daysCount) {
+    late Future<CountsByDayData> futureCountsByDay = ScrobblesApi.fetchCountsByDay(daysCount);
+
+    return BlocProvider<DataCountsByDayCubit>(
+      create: (BuildContext context) => DataCountsByDayCubit(),
+      child: BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
+        builder: (BuildContext context, DataCountsByDayState state) {
+          return FutureBuilder<CountsByDayData>(
+            future: futureCountsByDay,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataCountsByDayCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateCountsByHour(int daysCount) {
+    late Future<CountsByHourData> futureCountsByHour =
+        ScrobblesApi.fetchCountsByHour(daysCount);
+
+    return BlocProvider<DataCountsByHourCubit>(
+      create: (BuildContext context) => DataCountsByHourCubit(),
+      child: BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
+        builder: (BuildContext context, DataCountsByHourState state) {
+          return FutureBuilder<CountsByHourData>(
+            future: futureCountsByHour,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataCountsByHourCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateDiscoveries(int daysCount) {
+    late Future<DiscoveriesData> futureDiscoveries = ScrobblesApi.fetchDiscoveries(daysCount);
+
+    return BlocProvider<DataDiscoveriesCubit>(
+      create: (BuildContext context) => DataDiscoveriesCubit(),
+      child: BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
+        builder: (BuildContext context, DataDiscoveriesState state) {
+          return FutureBuilder<DiscoveriesData>(
+            future: futureDiscoveries,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataDiscoveriesCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateStatisticsGlobal() {
+    late Future<StatisticsGlobalData> futureStatisticsGlobal =
+        ScrobblesApi.fetchGlobalStatistics();
+
+    return BlocProvider<DataStatisticsGlobalCubit>(
+      create: (BuildContext context) => DataStatisticsGlobalCubit(),
+      child: BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
+        builder: (BuildContext context, DataStatisticsGlobalState dataState) {
+          return FutureBuilder<StatisticsGlobalData>(
+            future: futureStatisticsGlobal,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataStatisticsGlobalCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateStatisticsRecent(int daysCount) {
+    late Future<StatisticsRecentData> futureStatisticsRecent =
+        ScrobblesApi.fetchRecentStatistics(daysCount);
+
+    return BlocProvider<DataStatisticsRecentCubit>(
+      create: (BuildContext context) => DataStatisticsRecentCubit(),
+      child: BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
+        builder: (BuildContext context, DataStatisticsRecentState state) {
+          return FutureBuilder<StatisticsRecentData>(
+            future: futureStatisticsRecent,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataStatisticsRecentCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateTimeline(int daysCount) {
+    late Future<TimelineData> futureTimeline = ScrobblesApi.fetchTimeline(daysCount);
+
+    return BlocProvider<DataTimelineCubit>(
+      create: (BuildContext context) => DataTimelineCubit(),
+      child: BlocBuilder<DataTimelineCubit, DataTimelineState>(
+        builder: (BuildContext context, DataTimelineState state) {
+          return FutureBuilder<TimelineData>(
+            future: futureTimeline,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataTimelineCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  Widget updateTopArtists(int daysCount) {
+    late Future<TopArtistsData> futureTopArtists = ScrobblesApi.fetchTopArtists(daysCount);
+
+    return BlocProvider<DataTopArtistsCubit>(
+      create: (BuildContext context) => DataTopArtistsCubit(),
+      child: BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
+        builder: (BuildContext context, DataTopArtistsState state) {
+          return FutureBuilder<TopArtistsData>(
+            future: futureTopArtists,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return ShowErrorWidget(message: '${snapshot.error}');
+              }
+
+              BlocProvider.of<DataTopArtistsCubit>(context).setValue(snapshot.data);
+              return !snapshot.hasData ? loading : done;
+            },
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 33d49a5..0a0ceb2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
 
 publish_to: 'none'
 
-version: 0.0.28+28
+version: 0.0.29+29
 
 environment:
   sdk: '^3.0.0'
-- 
GitLab