Skip to content

5.5 State management: CRUD #10

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: theming
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,4 @@ packages-upgrade:
l10n:
flutter gen-l10n
appicon:
flutter pub run flutter_launcher_icons:main -f flutter_launcher_icons.yaml
deeplink:
@printf "Android:\nadb shell am start -a android.intent.action.VIEW -c andrmoid.intent.category.BROWSABLE -d de.coodoo.counter://settings"
@printf "\n\n"
@printf "iOS:\nxcrun simctl openurl booted de.coodoo.counter://settings"



flutter pub run flutter_launcher_icons:main -f flutter_launcher_icons.yaml
30 changes: 7 additions & 23 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.coodoo.counter_workshop">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.coodoo.counter_workshop">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="counter_workshop"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<application android:label="counter_workshop" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
</manifest>
4 changes: 1 addition & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import 'package:counter_workshop/src/app.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.db.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/remote/src/mock/counter_fake.api.dart';
import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart';
import 'package:flutter/material.dart';

void main() {
final CounterRepository counterRepository =
CounterRepository(counterApi: CounterFakeApi(), counterDatabase: CounterDatabase());
final CounterRepository counterRepository = CounterRepository(counterApi: CounterFakeApi());
runApp(
App(
counterRepository: counterRepository,
Expand Down
42 changes: 38 additions & 4 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,56 @@
import 'package:counter_workshop/src/core/theme/app.theme.dart';
import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart';
import 'package:counter_workshop/src/features/counter/presentation/counter.page.dart';
import 'package:counter_workshop/src/features/counter/presentation/dashboard/bloc/dashboard.bloc.dart';
import 'package:counter_workshop/src/features/counter/presentation/dashboard/bloc/dashboard.event.dart';
import 'package:counter_workshop/src/features/counter/presentation/dashboard/view/dashboard.page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class App extends StatelessWidget {
class App extends StatefulWidget {
const App({required this.counterRepository, super.key});

final CounterRepository counterRepository;

@override
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
late final DashboardBloc dashboardBloc;

@override
void initState() {
dashboardBloc = DashboardBloc(counterRepository: widget.counterRepository);
dashboardBloc.add(FetchCounterList());
super.initState();
}

@override
Widget build(BuildContext context) {
final appTheme = AppTheme();
return RepositoryProvider.value(
value: widget.counterRepository,
child: BlocProvider.value(
value: dashboardBloc,
child: const AppView(),
),
);
}
}

class AppView extends StatelessWidget {
const AppView({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
final appTheme = AppTheme();
return MaterialApp(
title: 'Counter Demo',
theme: appTheme.light,
darkTheme: appTheme.dark,
themeMode: ThemeMode.system,
home: CounterPage(counterRepository: counterRepository),
home: const DashboardPage(),
);
}
}
Empty file removed lib/src/core/extensions/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions lib/src/core/extensions/color.extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';

extension ColorExtension on String {
toColor() {
var hexString = this;
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
}
Empty file removed lib/src/core/routing/.gitkeep
Empty file.
4 changes: 3 additions & 1 deletion lib/src/core/theme/app.theme.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class AppTheme {
// Light Mode
Expand All @@ -25,11 +26,11 @@ class AppTheme {
final currentHeadlineColor = isLightMode ? headlineColor : headlineColorDark;

return base.copyWith(
brightness: isLightMode ? Brightness.light : Brightness.dark,
useMaterial3: true,
primaryColor: currentPrimaryColor,
scaffoldBackgroundColor: isLightMode ? scaffoldColor : scaffoldColorDark,
appBarTheme: base.appBarTheme.copyWith(
systemOverlayStyle: isLightMode ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light,
backgroundColor: Colors.transparent,
foregroundColor: currentPrimaryColor,
titleTextStyle: TextStyle(
Expand All @@ -38,6 +39,7 @@ class AppTheme {
color: currentPrimaryColor,
),
),
floatingActionButtonTheme: base.floatingActionButtonTheme.copyWith(backgroundColor: primaryColor),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
side: BorderSide(width: 2.0, color: currentHeadlineColor),
Expand Down
14 changes: 14 additions & 0 deletions lib/src/core/widgets/custom_loading_indicator.widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';

class CustomLoadingIndicator extends StatelessWidget {
const CustomLoadingIndicator({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(strokeWidth: 3),
);
}
}
23 changes: 23 additions & 0 deletions lib/src/core/widgets/error_message.widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';

class ErrorMessage extends StatelessWidget {
final Object error;

const ErrorMessage({
required this.error,
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Es ist ein Fehler aufgetreten:\n${error.toString()}',
style: const TextStyle(color: Colors.red, height: 1.5),
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:counter_workshop/src/features/counter/domain/model/counter.model.dart';

/// Locale app database like SqlLite that providers a [CounterModel]
class CounterDatabase {
CounterModel _counter = const CounterModel(id: '1', value: 0, name: 'A');
final int databaseDelay = 200;

Future<CounterModel> getCounter() {
// Pretend it's a db call
return Future.delayed(Duration(milliseconds: databaseDelay), () => _counter);
}

Future<void> storeCounter(CounterModel counter) {
_counter = counter;
if (_counter.value == 10) {
throw Exception('Database read lock while updating Counter to ${_counter.value}.');
} else {
// Pretend it's a db call
return Future.delayed(Duration(milliseconds: databaseDelay));
}
}
}
14 changes: 0 additions & 14 deletions lib/src/features/counter/data/datasources/local/counter.db.dart

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:counter_workshop/src/core/extensions/color.extension.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_request.dto.dart';
import 'package:counter_workshop/src/features/counter/domain/model/counter.model.dart';

class CounterRequestConverter {
CounterModel toModel(CounterRequestDto counterRequestDto) {
return CounterModel(
name: counterRequestDto.name,
value: counterRequestDto.counterValue,
stepSize: counterRequestDto.stepSize,
startValue: counterRequestDto.startValue,
color: counterRequestDto.color.toColor(),
goalValue: counterRequestDto.goalValue,
);
}

CounterRequestDto toDto(CounterModel counter) {
return CounterRequestDto(
name: counter.name,
counterValue: counter.value,
stepSize: counter.stepSize,
startValue: counter.value,
color: '#${(counter.color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0')}',
goalValue: counter.goalValue,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_response.dto.dart';
import 'package:counter_workshop/src/features/counter/domain/counter.model.dart';
import 'package:counter_workshop/src/features/counter/domain/model/counter.model.dart';
import 'package:counter_workshop/src/core/extensions/color.extension.dart';

class CounterResponseConverter {
CounterModel toModel(CounterResponseDto counterResponseDto) {
return CounterModel(
value: counterResponseDto.counterValue,
id: counterResponseDto.sysId,
name: counterResponseDto.name,
value: counterResponseDto.counterValue,
stepSize: counterResponseDto.stepSize,
startValue: counterResponseDto.startValue,
color: counterResponseDto.color.toColor(),
goalValue: counterResponseDto.goalValue,
);
}

CounterResponseDto toDto(CounterModel counter) {
return CounterResponseDto(
counterValue: counter.value,
sysId: counter.id,
name: counter.name,
counterValue: counter.value,
stepSize: counter.stepSize,
startValue: counter.value,
color: '#${(counter.color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0')}',
goalValue: counter.goalValue,
);
}
}
18 changes: 15 additions & 3 deletions lib/src/features/counter/data/datasources/remote/counter.api.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_request.dto.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_response.dto.dart';
import 'package:counter_workshop/src/features/counter/domain/counter.model.dart';
import 'package:counter_workshop/src/features/counter/domain/model/counter.model.dart';

/// The interface for a DataSource that provides access to a single [CounterModel]
abstract class CounterApi {
/// Fetches all counters
Future<List<CounterResponseDto>> fetchAll();

/// Fetches a counter with the give [id]
///
/// If no counter with the given id exits, a [CounterNotFoundException] error is thrown.
Future<CounterResponseDto> fetchCounter(String id);

/// Update the value [value] of a given counter [id]
/// Update the counter [CounterResponseDto] of a given counter [id]
///
/// If no counter with the given id exits, a [CounterNotFoundException] error is thrown.
Future<void> updateCounter(String id, CounterResponseDto counterResponseDto);

/// Create a new counter
Future<CounterResponseDto> createCounter(CounterRequestDto counterRequestDto);

/// Deletes a counter by a given counter [id]
///
/// If no counter with the given id exits, a [CounterNotFoundException] error is thrown.
Future<void> updateCounter(String id, int value);
Future<void> deleteCounter(String id);
}

/// Error thrown when a [CounterModel] is not found.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class CounterRequestDto {
CounterRequestDto({
required this.name,
required this.counterValue,
this.stepSize = 1,
this.startValue = 0,
this.color = '#ff3300',
this.goalValue,
this.createdAt,
this.updatedAt,
});

final String name;
final int counterValue;
final int startValue;
final int stepSize;
final String color;
final int? goalValue;
final DateTime? createdAt;
final DateTime? updatedAt;
}
Loading