Skip to content
Snippets Groups Projects
Commit 8de3f0ea authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Merge branch '43-add-api-call-and-allow-persist-display-refresh-data' into 'master'

Resolve "Add API call, and allow persist, display, refresh data"

Closes #43

See merge request !41
parents 7ad9a69c 44c671c6
No related branches found
No related tags found
1 merge request!41Resolve "Add API call, and allow persist, display, refresh data"
Pipeline #4867 passed
Showing
with 325 additions and 6 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=1.0.36
app.versionCode=37
app.versionName=1.0.37
app.versionCode=38
......@@ -2,10 +2,13 @@
"app_name": "Sandbox App",
"bottom_nav_sample": "Sample",
"bottom_nav_api": "API",
"bottom_nav_chart": "Graph",
"bottom_nav_settings": "Settings",
"bottom_nav_about": "About",
"api_page_title": "API",
"settings_title": "Settings",
"settings_label_api_url": "API URL:",
"settings_label_security_token": "Security token:",
......
......@@ -2,10 +2,13 @@
"app_name": "App de test",
"bottom_nav_sample": "Démo",
"bottom_nav_api": "API",
"bottom_nav_chart": "Graph",
"bottom_nav_settings": "Paramètres",
"bottom_nav_about": "À propos",
"api_page_title": "API",
"settings_title": "Paramètres",
"settings_label_api_url": "URL de l'API :",
"settings_label_security_token": "Jeton de sécurité :",
......
import 'package:equatable/equatable.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:random/models/api_failure.dart';
import 'package:random/models/api_data.dart';
import 'package:random/repository/api.dart';
part 'api_state.dart';
class ApiDataCubit extends HydratedCubit<ApiDataState> {
final ApiRepository apiRepository;
ApiDataCubit({required this.apiRepository}) : super(ApiDataFetchInitial());
void customEmit(ApiDataState state) {
String stateAsString = '';
if (state is ApiDataFetchLoaded) {
stateAsString = 'ApiDataFetchLoaded';
} else {
if (state is ApiDataFetchLoading) {
stateAsString = 'ApiDataFetchLoading';
} else {
if (state is ApiDataFetchError) {
stateAsString = 'ApiDataFetchError';
} else {
stateAsString = 'unknown';
}
}
}
print('emit new state: ' + stateAsString);
emit(state);
}
Future<ApiData> fetchApiData() async {
customEmit(ApiDataFetchLoading());
try {
final ApiData apiData = await apiRepository.getApiData();
customEmit(ApiDataFetchLoaded(data: apiData));
return apiData;
} on ApiFailure catch (err) {
customEmit(ApiDataFetchError(failure: err));
} catch (err) {
print("Error (fetchApiData): $err");
}
return ApiData.fromJson({});
}
@override
ApiDataState? fromJson(Map<String, dynamic> json) {
return ApiDataState(
data: ApiData.fromJson(json['api-data']),
);
}
@override
Map<String, Object?>? toJson(ApiDataState state) {
return <String, Object?>{
'api-data': state.data?.toJson(),
};
}
}
part of 'api_cubit.dart';
class ApiDataState extends Equatable {
final ApiData? data;
const ApiDataState({
this.data,
});
@override
List<Object> get props => [];
}
class ApiDataFetchInitial extends ApiDataState {}
class ApiDataFetchLoading extends ApiDataState {}
class ApiDataFetchLoaded extends ApiDataState {
final ApiData data;
const ApiDataFetchLoaded({
required this.data,
});
@override
List<Object> get props => [data];
}
class ApiDataFetchError extends ApiDataState {
final ApiFailure failure;
const ApiDataFetchError({
required this.failure,
});
@override
List<Object> get props => [failure];
}
......@@ -3,7 +3,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
class BottomNavCubit extends HydratedCubit<int> {
BottomNavCubit() : super(0);
int pagesCount = 4;
int pagesCount = 5;
void updateIndex(int index) {
if (isIndexAllowed(index)) {
......@@ -20,8 +20,8 @@ class BottomNavCubit extends HydratedCubit<int> {
void goToHomePage() => emit(0);
@override
int? fromJson(Map<String, dynamic> json) {
return json['pageIndex'] as int?;
int fromJson(Map<String, dynamic> json) {
return 0;
}
@override
......
......@@ -11,6 +11,9 @@ import 'package:random/config/theme.dart';
import 'package:random/cubit/bottom_nav_cubit.dart';
import 'package:random/cubit/data_cubit.dart';
import 'package:random/cubit/settings_cubit.dart';
import 'package:random/cubit/api_cubit.dart';
import 'package:random/repository/api.dart';
import 'package:random/network/api.dart';
import 'package:random/ui/skeleton.dart';
void main() async {
......@@ -47,6 +50,13 @@ class MyApp extends StatelessWidget {
BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()),
BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
BlocProvider<DataCubit>(create: (context) => DataCubit()),
BlocProvider<ApiDataCubit>(
create: (context) => ApiDataCubit(
apiRepository: ApiRepository(
apiService: ApiService(),
),
)..fetchApiData(),
),
],
child: MaterialApp(
title: 'Random application',
......
import 'dart:convert';
class ApiData {
final int? number;
final String? md5;
final DateTime? updatedAt;
const ApiData({
required this.number,
required this.md5,
required this.updatedAt,
});
factory ApiData.fromJson(Map<String, dynamic>? json) {
return ApiData(
number: (json?['number'] != null) ? (json?['number'] as int) : null,
md5: (json?['md5'] != null) ? (json?['md5'] as String) : null,
updatedAt: (json?['now'] != null) ? DateTime.parse(json?['now']) : null,
);
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'number': this.number ?? 0,
'md5': this.md5 ?? '',
'updatedAt': this.updatedAt?.toString(),
};
}
String toString() {
return jsonEncode(this.toJson());
}
}
class ApiFailure {
final String message;
final String code;
const ApiFailure({
this.message = '',
this.code = '',
});
}
enum ApiStatus {
loading,
loaded,
failed,
}
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:random/models/api_failure.dart';
class ApiService {
final Dio _dio = Dio();
final String baseUrl = 'https://tools.harrault.fr/tools/api';
Future<Response?> getData() async {
String url = baseUrl + '/get.php';
try {
print('fetching api data... ' + url);
final Response? response = await _dio.get(url);
print('ok got api response.');
print(response);
return response;
} on SocketException {
throw const ApiFailure(message: 'Failed to reach API endpoint.');
} catch (err) {
print("Error (getData): $err");
throw ApiFailure(message: "$err");
}
}
}
import 'package:random/models/api_data.dart';
import 'package:random/network/api.dart';
class ApiRepository {
const ApiRepository({required this.apiService});
final ApiService apiService;
Future<ApiData> getApiData() async {
print('(getApiData) delayed API call...');
final response = await Future.delayed(Duration(milliseconds: 1000))
.then((value) => apiService.getData());
if (response != null) {
print('(getApiData) got api response');
print(response.data);
return ApiData.fromJson(response.data);
}
print('(getApiData) failed');
return ApiData.fromJson({});
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:random/cubit/api_cubit.dart';
import 'package:random/models/api_status.dart';
import 'package:random/ui/widgets/api_data.dart';
import 'package:random/ui/widgets/header_app.dart';
class ApiPage extends StatelessWidget {
const ApiPage({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: <Widget>[
SizedBox(height: 8),
AppHeader(text: 'api_page_title'),
SizedBox(height: 20),
BlocBuilder<ApiDataCubit, ApiDataState>(
builder: (BuildContext context, ApiDataState apiDataState) {
return ApiDataWidget(
data: apiDataState.data,
status: (apiDataState is ApiDataFetchLoading)
? ApiStatus.loading
: (apiDataState is ApiDataFetchLoaded)
? ApiStatus.loaded
: ApiStatus.failed,
);
},
),
],
),
);
}
}
......@@ -4,6 +4,7 @@ import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:random/cubit/bottom_nav_cubit.dart';
import 'package:random/ui/screens/about_page.dart';
import 'package:random/ui/screens/api_page.dart';
import 'package:random/ui/screens/demo_page.dart';
import 'package:random/ui/screens/graph_page.dart';
import 'package:random/ui/screens/settings_page.dart';
......@@ -22,6 +23,7 @@ class _SkeletonScreenState extends State<SkeletonScreen> {
Widget build(BuildContext context) {
const List<Widget> pageNavigation = <Widget>[
DemoPage(),
ApiPage(),
GraphPage(),
SettingsPage(),
AboutPage(),
......
import 'package:flutter/material.dart';
import 'package:random/models/api_data.dart';
import 'package:random/models/api_status.dart';
import 'package:random/ui/widgets/error.dart';
class ApiDataWidget extends StatelessWidget {
const ApiDataWidget({
super.key,
required this.status,
required this.data,
});
final ApiStatus status;
final ApiData? data;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('status: ' + status.toString()),
Text('number: ' + (this.data?.number.toString() ?? '')),
Text('md5: ' + (this.data?.md5.toString() ?? '')),
Text('updatedAt: ' + (this.data?.updatedAt.toString() ?? '')),
status == ApiStatus.loading
? Container(
width: 12,
height: 12,
child: const CircularProgressIndicator(),
)
: const SizedBox.shrink(),
status == ApiStatus.failed
? ShowErrorWidget(message: 'state.failure.message')
: const SizedBox.shrink(),
],
);
}
}
......@@ -46,6 +46,10 @@ class BottomNavBar extends StatelessWidget {
icon: const Icon(UniconsLine.image),
label: tr('bottom_nav_sample'),
),
BottomNavigationBarItem(
icon: const Icon(UniconsLine.globe),
label: tr('bottom_nav_api'),
),
BottomNavigationBarItem(
icon: const Icon(UniconsLine.pen),
label: tr('bottom_nav_chart'),
......
......@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:random/cubit/data_cubit.dart';
import 'package:random/cubit/api_cubit.dart';
import 'package:random/cubit/settings_cubit.dart';
import 'package:random/models/interface_type.dart';
......@@ -26,6 +27,8 @@ class AppHeader extends StatelessWidget {
expertInterfaceIndicator(),
SizedBox(width: 2),
dataCounterIndicator(),
SizedBox(width: 2),
apiLoadingIndicator(),
],
);
}
......@@ -47,4 +50,16 @@ class AppHeader extends StatelessWidget {
},
);
}
Widget apiLoadingIndicator() {
return BlocBuilder<ApiDataCubit, ApiDataState>(
builder: (context, apiDataState) {
return (apiDataState is ApiDataFetchLoading)
? Text('⏳')
: (apiDataState is ApiDataFetchLoaded)
? Text('✅')
: Text('⚠️');
},
);
}
}
......@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
dio:
dependency: "direct main"
description:
name: dio
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
url: "https://pub.dev"
source: hosted
version: "5.3.3"
easy_localization:
dependency: "direct main"
description:
......
......@@ -3,7 +3,7 @@ description: A random application, for testing purpose only.
publish_to: 'none'
version: 1.0.36+37
version: 1.0.37+38
environment:
sdk: '^3.0.0'
......@@ -20,6 +20,7 @@ dependencies:
unicons: ^2.1.1
package_info_plus: ^5.0.1
flutter_swipe: ^1.0.1
dio: ^5.3.3
flutter:
uses-material-design: false
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment