Initial (redacted) commit.
This commit is contained in:
commit
655f8a036a
368 changed files with 20949 additions and 0 deletions
20
learning/demo-app/lib/l10n/app_de.arb
Normal file
20
learning/demo-app/lib/l10n/app_de.arb
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"appTitle": "Demo-App mit Riverpod und go_router",
|
||||
"ok": "Ok",
|
||||
"cancel": "Abbrechen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"add": "Hinzufügen",
|
||||
"save": "Speichern",
|
||||
"error": "Sorry, da ist etwas schief gegangen...",
|
||||
"loading": "Lade ...",
|
||||
"select": "Auswahl",
|
||||
"selected": "Ausgewählt",
|
||||
"search": "Suche",
|
||||
"counterLabel": "Du hast den Button sooft gedrückt:",
|
||||
"fabMainTooltip": "erhöhen",
|
||||
"placeholder": "placeholder",
|
||||
"home": "Start",
|
||||
"counter": "Zähler"
|
||||
}
|
27
learning/demo-app/lib/l10n/app_en.arb
Normal file
27
learning/demo-app/lib/l10n/app_en.arb
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "Demo app with riverpod and go_router",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"add": "Add",
|
||||
"save": "Save",
|
||||
"error": "We apologize for the inconvenience. This should not have happened...",
|
||||
"loading": "Loading",
|
||||
"select": "Select",
|
||||
"selected": "Selected",
|
||||
"deselect": "Deselect",
|
||||
"search": "Search",
|
||||
"counterLabel": "You have pushed the button this many times:",
|
||||
"fabMainTooltip": "Increment",
|
||||
"placeholder": "placeholder",
|
||||
"requiredField": "This field is required",
|
||||
"labelName": "Name",
|
||||
"labelImageUrl": "Url of an image",
|
||||
"delete": "Delete",
|
||||
"home": "Home",
|
||||
"counter": "Counter",
|
||||
"details": "Details",
|
||||
"noItems": "Sorry, no items."
|
||||
}
|
30
learning/demo-app/lib/main.dart
Normal file
30
learning/demo-app/lib/main.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'src/routing/app_router.dart';
|
||||
import 'src/utils/localization.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final goRouter = ref.watch(goRouterProvider);
|
||||
return MaterialApp.router(
|
||||
routerConfig: goRouter,
|
||||
debugShowCheckedModeBanner: false,
|
||||
onGenerateTitle: (context) => context.loc.appTitle,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
||||
useMaterial3: true,
|
||||
),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
// use to find missing semantics
|
||||
// showSemanticsDebugger: true,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'error_message_widget.dart';
|
||||
|
||||
//credits to Code with Andrea https://github.com/bizz84/starter_architecture_flutter_firebase/blob/master/lib/src/common_widgets/async_value_widget.dart
|
||||
class AsyncValueWidget<T> extends StatelessWidget {
|
||||
const AsyncValueWidget({required this.value, required this.data, super.key});
|
||||
final AsyncValue<T> value;
|
||||
final Widget Function(T) data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return value.when(
|
||||
data: data,
|
||||
error: (error, st) => Center(child: ErrorMessageWidget(error)),
|
||||
loading: () => const SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ri_go_demo/src/exceptions/api_exception.dart';
|
||||
|
||||
import '../utils/localization.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
//credits to Code with Andrea https://github.com/bizz84/starter_architecture_flutter_firebase/blob/master/lib/src/common_widgets/error_message_widget.dart
|
||||
|
||||
/// Simple reusable widget to show errors to the user.
|
||||
class ErrorMessageWidget extends StatelessWidget {
|
||||
const ErrorMessageWidget(this.error, {super.key});
|
||||
|
||||
/// Error object, might be a DioException.
|
||||
final Object? error;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
_pimpError(error, context.loc.error),
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.red),
|
||||
);
|
||||
}
|
||||
|
||||
String _pimpError(Object? error, String defaultStr) {
|
||||
if (error == null) {
|
||||
logger.d('ErrorMessageWidget - _pimpError - no error');
|
||||
return defaultStr;
|
||||
}
|
||||
try {
|
||||
final dioEx = error as DioException;
|
||||
if (dioEx.response != null) {
|
||||
final map = dioEx.response!.data as Map<String, dynamic>;
|
||||
if (map.containsKey('detail')) {
|
||||
return map['detail']! as String;
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.e(
|
||||
'ErrorMessageWidget - _pimpError - could not extract info',
|
||||
error: ex,
|
||||
);
|
||||
}
|
||||
try {
|
||||
final apiException = error as ApiException;
|
||||
return 'status ${apiException.statusCode}: ${apiException.message}';
|
||||
} catch (ex) {
|
||||
logger.e(
|
||||
'ErrorMessageWidget - _pimpError - could not extract message',
|
||||
error: ex,
|
||||
);
|
||||
}
|
||||
return defaultStr;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
//similar from code with Andrea
|
||||
class FormFieldWidget extends StatelessWidget {
|
||||
const FormFieldWidget({
|
||||
required this.controller,
|
||||
required this.labelText,
|
||||
super.key,
|
||||
this.keyboardType,
|
||||
this.formFieldKey,
|
||||
this.required = false,
|
||||
this.initialValue = '',
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String labelText;
|
||||
final TextInputType? keyboardType;
|
||||
final bool required;
|
||||
|
||||
final Key? formFieldKey;
|
||||
|
||||
final String initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Semantics(
|
||||
label: labelText,
|
||||
child: TextFormField(
|
||||
key: formFieldKey,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
),
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: keyboardType,
|
||||
validator: (value) {
|
||||
if (required && (value == null || value.isEmpty)) {
|
||||
return ''; //context.loc.requiredField;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
45
learning/demo-app/lib/src/common_widgets/primary_button.dart
Normal file
45
learning/demo-app/lib/src/common_widgets/primary_button.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
// credits to Code with Andrea, https://github.com/bizz84/starter_architecture_flutter_firebase/blob/master/lib/src/common_widgets/primary_button.dart
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
const PrimaryButton({
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
this.isEnabled = true,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
});
|
||||
final VoidCallback? onPressed;
|
||||
final String label;
|
||||
final bool isEnabled;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: isEnabled ? onPressed : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
backgroundColor ?? Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
foregroundColor ?? Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../constants/breakpoint.dart';
|
||||
|
||||
class TwoPanelWidget extends StatelessWidget {
|
||||
const TwoPanelWidget({
|
||||
required this.firstPanel,
|
||||
required this.secondPanel,
|
||||
super.key,
|
||||
});
|
||||
final Widget firstPanel;
|
||||
final Widget secondPanel;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final isWideScreen = screenSize.width > Breakpoint.tablet;
|
||||
|
||||
if (isWideScreen) {
|
||||
// Display master and detail side by side
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: firstPanel,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: secondPanel,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Display master and detail vertically
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: firstPanel,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: secondPanel,
|
||||
),
|
||||
], // Display the first item initially
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
6
learning/demo-app/lib/src/constants/api.dart
Normal file
6
learning/demo-app/lib/src/constants/api.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
// https://api-generator.retool.com/grQMFP/crud-demo/
|
||||
abstract class Api {
|
||||
static const String schema = 'https';
|
||||
static const String host = 'api-generator.retool.com';
|
||||
static const String path = 'grQMFP/crud-demo/';
|
||||
}
|
3
learning/demo-app/lib/src/constants/breakpoint.dart
Normal file
3
learning/demo-app/lib/src/constants/breakpoint.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
class Breakpoint {
|
||||
static const double tablet = 600;
|
||||
}
|
5
learning/demo-app/lib/src/constants/ui_constants.dart
Normal file
5
learning/demo-app/lib/src/constants/ui_constants.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
abstract class UIConstants {
|
||||
static const double minHitTargetHeight = 55;
|
||||
static const double verticalItemSpace = 8;
|
||||
static const double defaultPadding = 12;
|
||||
}
|
8
learning/demo-app/lib/src/exceptions/api_exception.dart
Normal file
8
learning/demo-app/lib/src/exceptions/api_exception.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
class ApiException implements Exception {
|
||||
ApiException(this.statusCode, this.message);
|
||||
final int statusCode;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../utils/localization.dart';
|
||||
import 'counter_screen_controller.dart';
|
||||
|
||||
class CounterScreen extends ConsumerWidget {
|
||||
const CounterScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(counterProvider);
|
||||
final controller = ref.read(counterProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(context.loc.appTitle),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.loc.counterLabel,
|
||||
),
|
||||
Text(
|
||||
'$state',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: controller.increment,
|
||||
tooltip: context.loc.fabMainTooltip,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'counter_screen_controller.g.dart';
|
||||
|
||||
|
||||
/// Annotating a class by `@riverpod` defines a new shared
|
||||
/// state for your application,
|
||||
/// accessible using the generated [counterProvider].
|
||||
/// This class is both responsible for initializing the state (through the
|
||||
/// [build] method)
|
||||
/// and exposing ways to modify it (cf [increment]).
|
||||
@riverpod
|
||||
class Counter extends _$Counter {
|
||||
/// Classes annotated by `@riverpod` **must** define a [build] function.
|
||||
/// This function is expected to return the initial state of your
|
||||
/// shared state.
|
||||
/// It is totally acceptable for this function to return a [Future] or
|
||||
/// [Stream] if you need to.
|
||||
/// You can also freely define parameters on this method.
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void increment() => state++;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'counter_screen_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$counterHash() => r'4243b34530f53accfd9014a9f0e316fe304ada3e';
|
||||
|
||||
/// Annotating a class by `@riverpod` defines a new shared
|
||||
/// state for your application,
|
||||
/// accessible using the generated [counterProvider].
|
||||
/// This class is both responsible for initializing the state (through the
|
||||
/// [build] method)
|
||||
/// and exposing ways to modify it (cf [increment]).
|
||||
///
|
||||
/// Copied from [Counter].
|
||||
@ProviderFor(Counter)
|
||||
final counterProvider = AutoDisposeNotifierProvider<Counter, int>.internal(
|
||||
Counter.new,
|
||||
name: r'counterProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$counterHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Counter = AutoDisposeNotifier<int>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
|
@ -0,0 +1,124 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../../constants/api.dart';
|
||||
import '../../../exceptions/api_exception.dart';
|
||||
import '../../../utils/dio_provider.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../domain/person.dart';
|
||||
|
||||
part 'people_repository.g.dart';
|
||||
|
||||
class PeopleRepository {
|
||||
PeopleRepository({required this.dio});
|
||||
final Dio dio;
|
||||
|
||||
String _getUrl({int? id}) {
|
||||
final url =
|
||||
Uri(scheme: Api.schema, host: Api.host, path: Api.path).toString();
|
||||
if (id != null) {
|
||||
return '$url$id';
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Person> getPersonById({required int id}) async {
|
||||
final url = _getUrl(id: id);
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
final response = await dio.get<String>(url);
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final person =
|
||||
Person.fromJson(json.decode(response.data!) as Map<String, Object?>);
|
||||
return person;
|
||||
} else {
|
||||
throw ApiException(
|
||||
response.statusCode ?? -1,
|
||||
'getPersonById ${response.statusCode}, data=${response.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Person>> getPeople() async {
|
||||
logger.d('people_repository.getPeople');
|
||||
final url = _getUrl();
|
||||
final response = await dio.get<List<dynamic>>(url);
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final dataList = response.data!;
|
||||
return dataList
|
||||
.map(
|
||||
(personJson) => Person.fromJson(personJson as Map<String, Object?>),
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
throw ApiException(
|
||||
response.statusCode ?? -1,
|
||||
'getPeople ${response.statusCode}, data=${response.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Person> updatePerson({required Person person}) async {
|
||||
final url = _getUrl(id: person.id);
|
||||
final response = await dio.put<String>(url, data: person.toJson());
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final personUpdated =
|
||||
Person.fromJson(json.decode(response.data!) as Map<String, Object?>);
|
||||
return personUpdated;
|
||||
} else {
|
||||
throw ApiException(
|
||||
response.statusCode ?? -1,
|
||||
'updateOwner ${response.statusCode}, data=${response.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deletePerson(int id) async {
|
||||
final url = _getUrl(id: id);
|
||||
final response = await dio.delete<String>(url);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw ApiException(
|
||||
response.statusCode ?? -1,
|
||||
'deletePerson ${response.statusCode}, data=${response.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Person> savePerson({required Person person}) async {
|
||||
final url = _getUrl();
|
||||
// this api uses the id if it exists, hence in case of a post
|
||||
// we make sure, there is no id
|
||||
final response =
|
||||
await dio.post<String>(url, data: person.toJsonWithoutId());
|
||||
if (response.statusCode == 201 && response.data != null) {
|
||||
final newPerson =
|
||||
Person.fromJson(json.decode(response.data!) as Map<String, Object?>);
|
||||
return newPerson;
|
||||
} else {
|
||||
throw ApiException(
|
||||
response.statusCode ?? -1,
|
||||
'savePerson ${response.statusCode}, data=${response.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
PeopleRepository peopleRepository(PeopleRepositoryRef ref) =>
|
||||
PeopleRepository(dio: ref.read(dioProvider));
|
||||
|
||||
@riverpod
|
||||
Future<List<Person>> fetchPeople(FetchPeopleRef ref) async {
|
||||
logger.d('people_repository.fetchPeople');
|
||||
final repo = ref.read(peopleRepositoryProvider);
|
||||
return repo.getPeople();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Person> fetchPersonById(FetchPersonByIdRef ref, int id) async {
|
||||
final repo = ref.read(peopleRepositoryProvider);
|
||||
return repo.getPersonById(id: id);
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'people_repository.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$peopleRepositoryHash() => r'd0e91e6cbf45120cbcd670b6cc99fe71a9d76429';
|
||||
|
||||
/// See also [peopleRepository].
|
||||
@ProviderFor(peopleRepository)
|
||||
final peopleRepositoryProvider = AutoDisposeProvider<PeopleRepository>.internal(
|
||||
peopleRepository,
|
||||
name: r'peopleRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$peopleRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PeopleRepositoryRef = AutoDisposeProviderRef<PeopleRepository>;
|
||||
String _$fetchPeopleHash() => r'772e01d6c483daa24de83820a021601fa93618e7';
|
||||
|
||||
/// See also [fetchPeople].
|
||||
@ProviderFor(fetchPeople)
|
||||
final fetchPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
|
||||
fetchPeople,
|
||||
name: r'fetchPeopleProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$fetchPeopleHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef FetchPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
|
||||
String _$fetchPersonByIdHash() => r'ab1560261f3491819dc88719e855f1c9b973ed21';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef FetchPersonByIdRef = AutoDisposeFutureProviderRef<Person>;
|
||||
|
||||
/// See also [fetchPersonById].
|
||||
@ProviderFor(fetchPersonById)
|
||||
const fetchPersonByIdProvider = FetchPersonByIdFamily();
|
||||
|
||||
/// See also [fetchPersonById].
|
||||
class FetchPersonByIdFamily extends Family<AsyncValue<Person>> {
|
||||
/// See also [fetchPersonById].
|
||||
const FetchPersonByIdFamily();
|
||||
|
||||
/// See also [fetchPersonById].
|
||||
FetchPersonByIdProvider call(
|
||||
int id,
|
||||
) {
|
||||
return FetchPersonByIdProvider(
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FetchPersonByIdProvider getProviderOverride(
|
||||
covariant FetchPersonByIdProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.id,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'fetchPersonByIdProvider';
|
||||
}
|
||||
|
||||
/// See also [fetchPersonById].
|
||||
class FetchPersonByIdProvider extends AutoDisposeFutureProvider<Person> {
|
||||
/// See also [fetchPersonById].
|
||||
FetchPersonByIdProvider(
|
||||
this.id,
|
||||
) : super.internal(
|
||||
(ref) => fetchPersonById(
|
||||
ref,
|
||||
id,
|
||||
),
|
||||
from: fetchPersonByIdProvider,
|
||||
name: r'fetchPersonByIdProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$fetchPersonByIdHash,
|
||||
dependencies: FetchPersonByIdFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FetchPersonByIdFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final int id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FetchPersonByIdProvider && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, id.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
|
@ -0,0 +1,37 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'person.freezed.dart';
|
||||
part 'person.g.dart';
|
||||
|
||||
@freezed
|
||||
class Person with _$Person {
|
||||
const factory Person({
|
||||
required int id,
|
||||
required String name,
|
||||
required String imageUrl,
|
||||
}) = _Person;
|
||||
|
||||
factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
|
||||
}
|
||||
|
||||
extension JsonWithoutId on Person {
|
||||
String toJsonWithoutId() {
|
||||
final map = toJson();
|
||||
// ignore: cascade_invocations //remove returns the removed field!, cascade_invocations
|
||||
map.remove('id');
|
||||
return json.encode(map);
|
||||
}
|
||||
}
|
||||
|
||||
// String toJsonWithoutId(Person p) {
|
||||
// var a = p.toJson();
|
||||
// var b = a.remove('id');
|
||||
// var c = json.encode(a);
|
||||
|
||||
// // final map = p.toJson().remove('id');
|
||||
// // return json.encode(map);
|
||||
// return c;
|
||||
// }
|
|
@ -0,0 +1,192 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'person.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
Person _$PersonFromJson(Map<String, dynamic> json) {
|
||||
return _Person.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Person {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get imageUrl => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$PersonCopyWith<Person> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $PersonCopyWith<$Res> {
|
||||
factory $PersonCopyWith(Person value, $Res Function(Person) then) =
|
||||
_$PersonCopyWithImpl<$Res, Person>;
|
||||
@useResult
|
||||
$Res call({int id, String name, String imageUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PersonCopyWithImpl<$Res, $Val extends Person>
|
||||
implements $PersonCopyWith<$Res> {
|
||||
_$PersonCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? imageUrl = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
imageUrl: null == imageUrl
|
||||
? _value.imageUrl
|
||||
: imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_PersonCopyWith<$Res> implements $PersonCopyWith<$Res> {
|
||||
factory _$$_PersonCopyWith(_$_Person value, $Res Function(_$_Person) then) =
|
||||
__$$_PersonCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int id, String name, String imageUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_PersonCopyWithImpl<$Res>
|
||||
extends _$PersonCopyWithImpl<$Res, _$_Person>
|
||||
implements _$$_PersonCopyWith<$Res> {
|
||||
__$$_PersonCopyWithImpl(_$_Person _value, $Res Function(_$_Person) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? imageUrl = null,
|
||||
}) {
|
||||
return _then(_$_Person(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
imageUrl: null == imageUrl
|
||||
? _value.imageUrl
|
||||
: imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_Person with DiagnosticableTreeMixin implements _Person {
|
||||
const _$_Person(
|
||||
{required this.id, required this.name, required this.imageUrl});
|
||||
|
||||
factory _$_Person.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_PersonFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String imageUrl;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Person(id: $id, name: $name, imageUrl: $imageUrl)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'Person'))
|
||||
..add(DiagnosticsProperty('id', id))
|
||||
..add(DiagnosticsProperty('name', name))
|
||||
..add(DiagnosticsProperty('imageUrl', imageUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Person &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.imageUrl, imageUrl) ||
|
||||
other.imageUrl == imageUrl));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, name, imageUrl);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_PersonCopyWith<_$_Person> get copyWith =>
|
||||
__$$_PersonCopyWithImpl<_$_Person>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_PersonToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Person implements Person {
|
||||
const factory _Person(
|
||||
{required final int id,
|
||||
required final String name,
|
||||
required final String imageUrl}) = _$_Person;
|
||||
|
||||
factory _Person.fromJson(Map<String, dynamic> json) = _$_Person.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get imageUrl;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_PersonCopyWith<_$_Person> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'person.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_Person _$$_PersonFromJson(Map<String, dynamic> json) => _$_Person(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_PersonToJson(_$_Person instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'imageUrl': instance.imageUrl,
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import '../../../common_widgets/async_value_widget.dart';
|
||||
import '../../../constants/ui_constants.dart';
|
||||
import '../../../utils/localization.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../data/people_repository.dart';
|
||||
import '../domain/person.dart';
|
||||
|
||||
class DetailsScreen extends ConsumerWidget {
|
||||
const DetailsScreen({super.key, this.id, this.person});
|
||||
final int? id;
|
||||
final Person? person;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (id == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.loc.details),
|
||||
),
|
||||
body: Text(context.loc.error),
|
||||
);
|
||||
} else if (person != null) {
|
||||
return DetailsScreenPlainWidget(person!);
|
||||
} else {
|
||||
// e.g. navigated to details by bookmark
|
||||
return AsyncValueWidget<Person>(
|
||||
value: ref.watch(fetchPersonByIdProvider(id!)),
|
||||
data: DetailsScreenPlainWidget.new,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsScreenPlainWidget extends StatelessWidget {
|
||||
const DetailsScreenPlainWidget(this.person, {super.key});
|
||||
final Person person;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.loc.details),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(UIConstants.defaultPadding),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('hi ${person.name}'),
|
||||
const Gap(UIConstants.verticalItemSpace),
|
||||
if (person.imageUrl.startsWith('http'))
|
||||
Expanded(
|
||||
child: Image(
|
||||
image: NetworkImage(person.imageUrl),
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
logger.d(
|
||||
'DetailsScreen - image, url=${person.imageUrl}',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return Text(person.imageUrl);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(person.imageUrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../../utils/logger.dart';
|
||||
import '../data/people_repository.dart';
|
||||
import '../domain/person.dart';
|
||||
|
||||
part 'edit_person_controller.g.dart';
|
||||
|
||||
// in some cases it might be good to store the current selected item in a
|
||||
// separate provider
|
||||
// final currentPersonProvider = StateProvider<Person?>((ref) {
|
||||
// return null;
|
||||
// });
|
||||
|
||||
@riverpod
|
||||
class EditPersonController extends _$EditPersonController {
|
||||
@override
|
||||
FutureOr<Person?> build() {
|
||||
ref.onDispose(
|
||||
() => logger.i('EditPersonController ----- dispose controller -----'),
|
||||
);
|
||||
state = const AsyncData(null);
|
||||
return state.value;
|
||||
}
|
||||
|
||||
//for non string-based setters and hence input fields without own controllers
|
||||
//use setters like
|
||||
// void setCheckboxField(bool value) {
|
||||
// state = AsyncData(state.value!.copyWith(isBossy: value));
|
||||
// }
|
||||
|
||||
/// Show the form with the values given by [person].
|
||||
void editPerson(Person person) {
|
||||
state = AsyncData(person);
|
||||
}
|
||||
|
||||
/// Show the form with empty fields, ready to create a new person.
|
||||
void newPerson() {
|
||||
state = const AsyncData(Person(id: -1, name: '', imageUrl: ''));
|
||||
}
|
||||
|
||||
Future<void> delete() async {
|
||||
if (state.value == null || state.value!.id < 0) {
|
||||
return;
|
||||
}
|
||||
state = const AsyncLoading<Person?>();
|
||||
try {
|
||||
final repo = ref.read(peopleRepositoryProvider);
|
||||
await repo.deletePerson(state.value!.id);
|
||||
//this provider does not know this change, hence we need to force a
|
||||
//refresh by invalidating it
|
||||
ref.invalidate(fetchPeopleProvider);
|
||||
state = const AsyncData(null);
|
||||
} catch (error) {
|
||||
state = AsyncError(error, StackTrace.current);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save({required String name, required String imageUrl}) async {
|
||||
if (name.isEmpty || state.value == null) {
|
||||
return;
|
||||
}
|
||||
// use copywith to keep id
|
||||
final editedPerson = state.value!.copyWith(name: name, imageUrl: imageUrl);
|
||||
// to keep changes of the form fields, even if we may have an error
|
||||
state = AsyncData(editedPerson);
|
||||
state = const AsyncLoading<Person?>();
|
||||
try {
|
||||
final repo = ref.read(peopleRepositoryProvider);
|
||||
if (editedPerson.id < 0) {
|
||||
await repo.savePerson(person: editedPerson);
|
||||
} else {
|
||||
await repo.updatePerson(person: editedPerson);
|
||||
}
|
||||
ref.invalidate(fetchPeopleProvider);
|
||||
state = const AsyncData(null);
|
||||
} catch (error) {
|
||||
state = AsyncError<Person?>(error, StackTrace.current);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'edit_person_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$editPersonControllerHash() =>
|
||||
r'7c656c78c7d8c60e746ec6bd7ad4e089f2f3fcac';
|
||||
|
||||
/// See also [EditPersonController].
|
||||
@ProviderFor(EditPersonController)
|
||||
final editPersonControllerProvider =
|
||||
AutoDisposeAsyncNotifierProvider<EditPersonController, Person?>.internal(
|
||||
EditPersonController.new,
|
||||
name: r'editPersonControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$editPersonControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$EditPersonController = AutoDisposeAsyncNotifier<Person?>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import '../../../common_widgets/error_message_widget.dart';
|
||||
import '../../../common_widgets/form_field_widget.dart';
|
||||
import '../../../common_widgets/primary_button.dart';
|
||||
import '../../../constants/ui_constants.dart';
|
||||
import '../../../utils/localization.dart';
|
||||
import 'edit_person_controller.dart';
|
||||
|
||||
class EditPersonForm extends ConsumerStatefulWidget {
|
||||
const EditPersonForm({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<EditPersonForm> createState() => _EditPersonFormState();
|
||||
}
|
||||
|
||||
class _EditPersonFormState extends ConsumerState<EditPersonForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
static const Key _nameKey = Key('name');
|
||||
static const Key _imageUrlKey = Key('image_url');
|
||||
final _nameController = TextEditingController();
|
||||
final _imageUrlController = TextEditingController();
|
||||
late final EditPersonController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = ref.read(editPersonControllerProvider.notifier);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_imageUrlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(editPersonControllerProvider);
|
||||
if (state.isLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
if (state.value == null) {
|
||||
return Container();
|
||||
} else {
|
||||
_nameController.text = state.value!.name;
|
||||
_imageUrlController.text = state.value!.imageUrl;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UIConstants.defaultPadding),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
//put here to have both visible, the error and the form
|
||||
if (state.hasError) ErrorMessageWidget(state.error),
|
||||
FormFieldWidget(
|
||||
formFieldKey: _nameKey,
|
||||
controller: _nameController,
|
||||
labelText: context.loc.labelName,
|
||||
required: true,
|
||||
),
|
||||
const Gap(UIConstants.verticalItemSpace),
|
||||
FormFieldWidget(
|
||||
formFieldKey: _imageUrlKey,
|
||||
controller: _imageUrlController,
|
||||
labelText: context.loc.labelImageUrl,
|
||||
),
|
||||
const Gap(UIConstants.verticalItemSpace),
|
||||
Row(
|
||||
children: [
|
||||
PrimaryButton(
|
||||
label: context.loc.delete,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
onPressed:
|
||||
state.isLoading ? null : () => {_controller.delete()},
|
||||
isEnabled: state.value!.id > 0,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: context.loc.save,
|
||||
onPressed: state.isLoading
|
||||
? null
|
||||
: () => {
|
||||
if (_formKey.currentState!.validate())
|
||||
{
|
||||
_controller.save(
|
||||
name: _nameController.text,
|
||||
imageUrl: _imageUrlController.text,
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../common_widgets/async_value_widget.dart';
|
||||
import '../../../common_widgets/two_panel_widget.dart';
|
||||
import '../../../routing/app_router.dart';
|
||||
import '../../../utils/localization.dart';
|
||||
import '../data/people_repository.dart';
|
||||
import '../domain/person.dart';
|
||||
import 'edit_person_controller.dart';
|
||||
import 'edit_person_form.dart';
|
||||
|
||||
class PeopleScreen extends ConsumerWidget {
|
||||
const PeopleScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(context.loc.appTitle),
|
||||
),
|
||||
body: TwoPanelWidget(
|
||||
firstPanel: RefreshIndicator(
|
||||
//pull to refresh
|
||||
onRefresh: () {
|
||||
ref.invalidate(fetchPeopleProvider);
|
||||
return ref.read(fetchPeopleProvider.future);
|
||||
},
|
||||
child: AsyncValueWidget<List<Person>>(
|
||||
value: ref.watch(fetchPeopleProvider),
|
||||
data: (people) => people.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
context.loc.noItems,
|
||||
style: Theme.of(context).textTheme.displaySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: people.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(people[index].name),
|
||||
onTap: () {
|
||||
context.goNamed(
|
||||
SubRoutes.details.name,
|
||||
pathParameters: {
|
||||
Parameter.id.name: people[index].id.toString(),
|
||||
},
|
||||
extra: people[index],
|
||||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'Edit '.hardcoded + people[index].name,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
editPersonControllerProvider.notifier,
|
||||
)
|
||||
.editPerson(people[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
secondPanel: const EditPersonForm(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
ref.read(editPersonControllerProvider.notifier).newPerson();
|
||||
},
|
||||
tooltip: 'click to add a new person'.hardcoded,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
learning/demo-app/lib/src/routing/app_router.dart
Normal file
109
learning/demo-app/lib/src/routing/app_router.dart
Normal file
|
@ -0,0 +1,109 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../features/counter/presentation/counter_screen.dart';
|
||||
import '../features/rest_crud_demo/domain/person.dart';
|
||||
import '../features/rest_crud_demo/presentation/details_screen.dart';
|
||||
import '../features/rest_crud_demo/presentation/people_screen.dart';
|
||||
import 'scaffold_with_navigation.dart';
|
||||
|
||||
part 'app_router.g.dart';
|
||||
|
||||
// general ideas on navigation see https://m2.material.io/design/navigation/understanding-navigation.html#forward-navigation
|
||||
|
||||
// shell routes, appear in the bottom navigation
|
||||
// see https://pub.dev/documentation/go_router/latest/go_router/ShellRoute-class.html
|
||||
enum TopLevelDestinations { people, counter }
|
||||
|
||||
// GlobalKey is a factory, hence each call creates a key
|
||||
//this is root, even if it navigates to people, it needs a separate key!!!
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final _peopleNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: TopLevelDestinations.people.name);
|
||||
final _counterNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: TopLevelDestinations.counter.name);
|
||||
|
||||
// other destinations, reachable from a top level destination
|
||||
enum SubRoutes { details }
|
||||
|
||||
enum Parameter { id }
|
||||
|
||||
//https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GoRouter goRouter(GoRouterRef ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/${TopLevelDestinations.people.name}',
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
debugLogDiagnostics: true,
|
||||
routes: [
|
||||
// Stateful navigation based on:
|
||||
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) {
|
||||
return ScaffoldWithNavigation(navigationShell: navigationShell);
|
||||
},
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _peopleNavigatorKey,
|
||||
routes: [
|
||||
// base route people
|
||||
GoRoute(
|
||||
path: '/${TopLevelDestinations.people.name}', // path: /people
|
||||
name: TopLevelDestinations.people.name,
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const PeopleScreen(),
|
||||
),
|
||||
routes: <RouteBase>[
|
||||
// The details screen to display stacked on navigator of the
|
||||
// first tab. This will cover screen A but not the application
|
||||
// shell (bottom navigation bar).
|
||||
GoRoute(
|
||||
path: '${SubRoutes.details.name}/:${Parameter.id.name}',
|
||||
name: SubRoutes.details.name,
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
// alternatively use https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html
|
||||
final id =
|
||||
int.parse(state.pathParameters[Parameter.id.name]!);
|
||||
final person = _extractPersonFromExtra(state.extra);
|
||||
return DetailsScreen(id: id, person: person);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _counterNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/${TopLevelDestinations.counter.name}',
|
||||
name: TopLevelDestinations.counter.name,
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const CounterScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Person? _extractPersonFromExtra(Object? extra) {
|
||||
return extra == null
|
||||
? null
|
||||
: extra is Person
|
||||
? extra
|
||||
: extra is Map // if you come back from bottom navigation, e.g. look
|
||||
// at details of a person, go to counter via bottom navigation,
|
||||
// use bottom navigation to go to people/home
|
||||
? Person.fromJson(
|
||||
extra as Map<String, Object?>,
|
||||
)
|
||||
: null;
|
||||
}
|
23
learning/demo-app/lib/src/routing/app_router.g.dart
Normal file
23
learning/demo-app/lib/src/routing/app_router.g.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_router.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$goRouterHash() => r'caf38f1ae54f10aea6450bd6fc62dae849543827';
|
||||
|
||||
/// See also [goRouter].
|
||||
@ProviderFor(goRouter)
|
||||
final goRouterProvider = Provider<GoRouter>.internal(
|
||||
goRouter,
|
||||
name: r'goRouterProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$goRouterHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef GoRouterRef = ProviderRef<GoRouter>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
133
learning/demo-app/lib/src/routing/scaffold_with_navigation.dart
Normal file
133
learning/demo-app/lib/src/routing/scaffold_with_navigation.dart
Normal file
|
@ -0,0 +1,133 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../constants/breakpoint.dart';
|
||||
import '../utils/localization.dart';
|
||||
|
||||
// see https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
|
||||
// and https://github.com/bizz84/tmdb_movie_app_riverpod/blob/main/lib/src/routing/scaffold_with_nested_navigation.dart
|
||||
class NavigationItem {
|
||||
NavigationItem({required this.icon, required this.selectedIcon});
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
}
|
||||
|
||||
final _navigationList = (
|
||||
people: NavigationItem(icon: Icons.home_outlined, selectedIcon: Icons.home),
|
||||
counter: NavigationItem(
|
||||
icon: Icons.plus_one_outlined,
|
||||
selectedIcon: Icons.plus_one,
|
||||
),
|
||||
);
|
||||
|
||||
class ScaffoldWithNavigation extends StatelessWidget {
|
||||
const ScaffoldWithNavigation({
|
||||
required this.navigationShell,
|
||||
Key? key,
|
||||
}) : super(key: key ?? const ValueKey('ScaffoldWithNavigation'));
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
void _goBranch(int index) {
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
if (size.width < Breakpoint.tablet) {
|
||||
return ScaffoldWithNavigationBar(
|
||||
body: navigationShell,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
} else {
|
||||
return ScaffoldWithNavigationRail(
|
||||
body: navigationShell,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavigationBar extends StatelessWidget {
|
||||
const ScaffoldWithNavigationBar({
|
||||
required this.body,
|
||||
required this.currentIndex,
|
||||
required this.onDestinationSelected,
|
||||
super.key,
|
||||
});
|
||||
final Widget body;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: currentIndex,
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: Icon(_navigationList.people.icon),
|
||||
selectedIcon: Icon(_navigationList.people.selectedIcon),
|
||||
label: context.loc.home,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(_navigationList.counter.icon),
|
||||
selectedIcon: Icon(_navigationList.counter.selectedIcon),
|
||||
label: context.loc.counter,
|
||||
),
|
||||
],
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavigationRail extends StatelessWidget {
|
||||
const ScaffoldWithNavigationRail({
|
||||
required this.body,
|
||||
required this.currentIndex,
|
||||
required this.onDestinationSelected,
|
||||
super.key,
|
||||
});
|
||||
final Widget body;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: <NavigationRailDestination>[
|
||||
NavigationRailDestination(
|
||||
icon: Icon(_navigationList.people.icon),
|
||||
selectedIcon: Icon(_navigationList.people.selectedIcon),
|
||||
label: Text(context.loc.home),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(_navigationList.counter.icon),
|
||||
selectedIcon: Icon(_navigationList.counter.selectedIcon),
|
||||
label: Text(context.loc.counter),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalDivider(thickness: 1, width: 1),
|
||||
// This is the main content.
|
||||
Expanded(
|
||||
child: body,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
18
learning/demo-app/lib/src/utils/dio_provider.dart
Normal file
18
learning/demo-app/lib/src/utils/dio_provider.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'dio_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Dio dio(DioRef ref) {
|
||||
final dio = Dio();
|
||||
dio.interceptors.add(
|
||||
PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
responseBody: false,
|
||||
responseHeader: true,
|
||||
),
|
||||
);
|
||||
return dio;
|
||||
}
|
23
learning/demo-app/lib/src/utils/dio_provider.g.dart
Normal file
23
learning/demo-app/lib/src/utils/dio_provider.g.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'dio_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$dioHash() => r'50bc684111bbcb930ccaac200da3a7ad761e689b';
|
||||
|
||||
/// See also [dio].
|
||||
@ProviderFor(dio)
|
||||
final dioProvider = AutoDisposeProvider<Dio>.internal(
|
||||
dio,
|
||||
name: r'dioProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef DioRef = AutoDisposeProviderRef<Dio>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
14
learning/demo-app/lib/src/utils/localization.dart
Normal file
14
learning/demo-app/lib/src/utils/localization.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/// A simple placeholder that can be used to search all the hardcoded strings
|
||||
/// in the code (useful to identify strings that need to be localized).
|
||||
/// thanks to https://github.com/bizz84/starter_architecture_flutter_firebase/blob/master/lib/src/localization/string_hardcoded.dart
|
||||
extension StringHardcoded on String {
|
||||
String get hardcoded => this;
|
||||
}
|
||||
|
||||
//thanks to https://codewithandrea.com/articles/flutter-localization-build-context-extension/
|
||||
extension LocalizedBuildContext on BuildContext {
|
||||
AppLocalizations get loc => AppLocalizations.of(this);
|
||||
}
|
13
learning/demo-app/lib/src/utils/logger.dart
Normal file
13
learning/demo-app/lib/src/utils/logger.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
import 'package:logger/logger.dart';
|
||||
|
||||
const Level loggerLevel = Level.trace;
|
||||
|
||||
/// Logger for the app.
|
||||
Logger logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 1,
|
||||
errorMethodCount: 5,
|
||||
lineLength: 90,
|
||||
),
|
||||
level: loggerLevel,
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue