Skip to content
Draft
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
5 changes: 4 additions & 1 deletion ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -171,10 +171,12 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
Expand All @@ -185,6 +187,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
11 changes: 10 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
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() {
runApp(const App());
final CounterRepository counterRepository =
CounterRepository(counterApi: CounterFakeApi(), counterDatabase: CounterDatabase());
runApp(
App(
counterRepository: counterRepository,
),
);
}
8 changes: 5 additions & 3 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:counter_workshop/src/features/counter/counter.page.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:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
Expand All @@ -11,7 +13,7 @@ class App extends StatelessWidget {
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CounterPage(),
home: CounterPage(counterRepository: counterRepository),
);
}
}
Empty file.
Empty file added lib/src/core/widgets/.gitkeep
Empty file.
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions lib/src/features/counter/data/datasources/local/counter.db.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:counter_workshop/src/features/counter/domain/counter.model.dart';

/// Locale app database like SqlLite that providers a [CounterModel]
class CounterDatabase {
CounterModel _counter = CounterModel(value: 0);

CounterModel getCounter() {
return _counter;
}

storeCounter(CounterModel counter) {
_counter = counter;
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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';

class CounterResponseConverter {
CounterModel toModel(CounterResponseDto counterResponseDto) {
return CounterModel(
value: counterResponseDto.counterValue,
id: counterResponseDto.sysId,
);
}

CounterResponseDto toDto(CounterModel counter) {
return CounterResponseDto(
counterValue: counter.value,
sysId: counter.id,
);
}
}
18 changes: 18 additions & 0 deletions lib/src/features/counter/data/datasources/remote/counter.api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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';

/// The interface for a DataSource that provides access to a single [CounterModel]
abstract class CounterApi {
/// 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]
///
/// If no counter with the given id exits, a [CounterNotFoundException] error is thrown.
Future<void> updateCounter(String id, int value);
}

/// Error thrown when a [CounterModel] is not found.
class CounterNotFoundException implements Exception {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CounterResponseDto {
CounterResponseDto({
required this.sysId,
required this.counterValue,
this.createdAt,
this.updatedAt,
});

final String sysId;
final int counterValue;
final DateTime? createdAt;
final DateTime? updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:counter_workshop/src/features/counter/data/datasources/remote/counter.api.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';

/// FakeApi that simulates a remote restful API which providers a [CounterModel]
class CounterFakeApi implements CounterApi {
@override
Future<CounterResponseDto> fetchCounter(String id) {
// simulate a network delay
return Future.delayed(const Duration(milliseconds: 300), () {
if (id == '1') {
// return a dummy counter
return CounterResponseDto(
counterValue: 0,
sysId: '1',
createdAt: DateTime.now(),
);
} else {
// return a exception
throw CounterNotFoundException();
}
});
}

@override
Future<void> updateCounter(String id, int value) {
return Future.delayed(const Duration(milliseconds: 300), () {
if (id == '1') {
return;
} else {
// return a exception
throw CounterNotFoundException();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:counter_workshop/src/features/counter/data/datasources/remote/counter.api.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:http/http.dart' as http;

/// Remote restful API that providers a [CounterModel]
class CounterRestApi implements CounterApi {
CounterRestApi({required this.client});
final http.Client client;

@override
Future<CounterResponseDto> fetchCounter(String id) {
// TODO: implement fetchCounter
throw UnimplementedError();
}

@override
Future<void> updateCounter(String id, int value) {
// TODO: implement incrementCounter
throw UnimplementedError();
}
}
40 changes: 40 additions & 0 deletions lib/src/features/counter/data/repositories/counter.repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:async';

import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.db.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/remote/counter.api.dart';
import 'package:counter_workshop/src/features/counter/data/datasources/remote/converters/counter_response.converter.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 'dart:developer';

class CounterRepository {
CounterRepository({required this.counterApi, required this.counterDatabase}) {
// prefill repository Counter from API
_fetchCounterData();
}

final CounterApi counterApi;
final CounterDatabase counterDatabase;
final String defaultCounterId = '1'; // TODO: allow multiple counters

Future<void> _fetchCounterData() async {
log('retriving default counter');
CounterResponseDto counterResponseDto = await counterApi.fetchCounter(defaultCounterId);

// map result to Model
CounterModel counterModel = CounterResponseConverter().toModel(counterResponseDto);

// store model in database
counterDatabase.storeCounter(counterModel);
}

CounterModel getCounter() {
return counterDatabase.getCounter();
}

Future<void> updateCounter({required CounterModel counterModel}) async {
log('updating counter: ${counterModel.id} with value: $counterModel');
await counterApi.updateCounter(counterModel.id, counterModel.value);
return;
}
}
17 changes: 17 additions & 0 deletions lib/src/features/counter/domain/counter.model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:equatable/equatable.dart';

// ignore: must_be_immutable
class CounterModel extends Equatable {
CounterModel({
this.value = 0,
this.id = '1',
});

/// technical counter id
final String id;

int value;

@override
List<Object?> get props => [id, value];
}
16 changes: 16 additions & 0 deletions lib/src/features/counter/presentation/counter.controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart';
import 'package:counter_workshop/src/features/counter/domain/counter.model.dart';

class CounterController {
CounterController({required this.counterRepository}) {
counterModel = counterRepository.getCounter();
}

final CounterRepository counterRepository;
CounterModel counterModel = CounterModel();

Future<void> increment() async {
counterModel.value += 1;
counterRepository.updateCounter(counterModel: counterModel);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart';
import 'package:counter_workshop/src/features/counter/presentation/counter.controller.dart';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
const CounterPage({super.key});
const CounterPage({required this.counterRepository, super.key});
final CounterRepository counterRepository;

@override
State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
int _counter = 0;
late final CounterController counterController;
@override
void initState() {
counterController = CounterController(counterRepository: widget.counterRepository);
super.initState();
}

void _incrementCounter() {
setState(() {
_counter++;
counterController.increment();
});
}

Expand All @@ -30,7 +38,7 @@ class _CounterPageState extends State<CounterPage> {
'You have pushed the button this many times:',
),
Text(
'$_counter',
'${counterController.counterModel.value}',
style: Theme.of(context).textTheme.headline4,
),
],
Expand Down
Loading