Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

code refactoring and unit testing #10

Merged
merged 14 commits into from
Aug 15, 2019
28 changes: 11 additions & 17 deletions lib/Services/Db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import 'dart:async';
import 'dart:io';

import 'package:goalkeeper/Models/Goal.dart';
import 'package:goalkeeper/Services/Interfaces/IDatabase.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

class Db {
class Db implements IDatabase {
static Database _database; //singleton database object
static Db _databaseHelper =
new Db._createInstance(); //singleton database helper object
Future<Database> get database async {
if (_database == null) {
_database = await initDatabase();
}
return _database;
}

final String goalsTable = "goal_table";
final String colId = "id";
Expand All @@ -18,11 +23,7 @@ class Db {
final String colDeadLine = "deadLine";
final String dbName = "goals.db";

Db._createInstance();

factory Db() => _databaseHelper;

void _createDb(Database db, int newVersion) async {
void createDb(Database db, int newVersion) async {
await db.execute('''
CREATE TABLE $goalsTable (
$colId INTEGER PRIMARY KEY AUTOINCREMENT,
Expand All @@ -35,19 +36,12 @@ class Db {
Future<Database> initDatabase() async {
Directory directory = await getApplicationDocumentsDirectory();
String path = join(directory.path, dbName);
return await openDatabase(path, version: 1, onCreate: _createDb);
}

Future<Database> get database async {
if (_database == null) {
_database = await initDatabase();
}
return _database;
return await openDatabase(path, version: 1, onCreate: createDb);
}

Future<List<Map<String, dynamic>>> getGoalsMapList() async {
Database db = await this.database;
return await db.query(goalsTable);
return await db.query(goalsTable, orderBy: colId);
}

Future<int> createGoal(Goal goal) async {
Expand Down
43 changes: 43 additions & 0 deletions lib/Services/Factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:goalkeeper/Services/Interfaces/IDatabase.dart';
import 'package:goalkeeper/Services/Interfaces/IRepository.dart';
import 'package:goalkeeper/Services/NotificationCenter.dart';

class Factory {
static IDatabase databaseSingleton;
static IRepository repositorySingleton;
static NotificationCenter notificationCenterSingleton;

static Factory _instance = Factory.createInstance();
Factory.createInstance();
factory Factory() => _instance;

// each of the services must comply to the corresponding contract (i.e: interface - which is also class in dart..)
// thats why we bound the types with generics

void registerDatabase<T extends IDatabase>(T database) {
if (databaseSingleton == null) databaseSingleton = database;
}

void registerRepository<T extends IRepository>(T repository) {
if (repositorySingleton == null) repositorySingleton = repository;
}

void registerNotificationCenter<T extends NotificationCenter>(
T notificationCenter) {
if (notificationCenterSingleton == null)
notificationCenterSingleton = notificationCenter;
}

IDatabase get database => databaseSingleton != null
? databaseSingleton
: throw StateError('Register your dependencies before requisting them !');

IRepository get repository => repositorySingleton != null
? repositorySingleton
: throw StateError('Register your dependencies before requisting them !');

NotificationCenter get notificationCenter => notificationCenterSingleton !=
null
? notificationCenterSingleton
: throw StateError('Register your dependencies before requisting them !');
}
45 changes: 26 additions & 19 deletions lib/Services/GoalsRepository.dart
Original file line number Diff line number Diff line change
@@ -1,48 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'package:goalkeeper/Models/Goal.dart';
import 'package:goalkeeper/Services/Db.dart';
import 'package:goalkeeper/Services/Interfaces/ICache.dart';
import 'package:goalkeeper/Services/Interfaces/IRepository.dart';

class GoalsRepository {
import 'Interfaces/IDatabase.dart';

class GoalsRepository implements IRepository, ICache<Goal> {
List<Goal> _cache = new List<Goal>();
static final GoalsRepository instance = new GoalsRepository.getInstance();
GoalsRepository.getInstance();
IDatabase database;
GoalsRepository({@required this.database});

bool shouldUpdateCache = true;
bool shouldSyncCache = true;

Future<void> updateCache() async {
Future<void> syncCache() async {
if (_cache.isNotEmpty) _cache.clear();
var goalsList = await database.getGoalsList();
_cache.addAll(goalsList);
}

void purgeCache() {
_cache.clear();
updateCache();
shouldSyncCache = false;
}

int getGoalsCount() => _cache.length;

Future<List<Goal>> getGoalsList() async {
if (shouldUpdateCache) {
await updateCache();
shouldUpdateCache = false;
if (shouldSyncCache) {
await syncCache();
}
return _cache;
}

void insert(Goal goal) {
if (goal.id == null) goal.id = this.getNextId();
Db().createGoal(goal);
database.createGoal(goal);
_cache.add(goal);
}

void delete(Goal goal) {
Db().deleteGoal(goal);
database.deleteGoal(goal);
_cache.removeWhere((Goal _goal) => _goal.id == goal.id);
}

void update(Goal goal) {
Db().updateGoal(goal);
var itemIndex = _cache.indexOf(goal);
_cache.replaceRange(itemIndex, itemIndex + 1, [goal]);
database.updateGoal(goal);
var itemIndex = _cache.indexWhere((Goal _g) => _g.id == goal.id);
if (itemIndex == -1) return;

if (_cache.length == 1) {
_cache.clear();
_cache.add(goal);
} else {
_cache.replaceRange(itemIndex, itemIndex + 1, [goal]);
}
}

int getNextId() {
Expand Down
4 changes: 4 additions & 0 deletions lib/Services/Interfaces/ICache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
abstract class ICache<T> {
bool shouldSyncCache = true;
Future<void> syncCache();
}
9 changes: 9 additions & 0 deletions lib/Services/Interfaces/IDatabase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:goalkeeper/Models/Goal.dart';

abstract class IDatabase {
Future<int> createGoal(Goal goal);
Future<int> updateGoal(Goal goal);
Future<int> deleteGoal(Goal goal);
Future<int> getCount(Goal goal);
Future<List<Goal>> getGoalsList();
}
11 changes: 11 additions & 0 deletions lib/Services/Interfaces/IRepository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:goalkeeper/Models/Goal.dart';

abstract class IRepository {
int getGoalsCount();
void insert(Goal goal);
void delete(Goal goal);
void update(Goal goal);
Goal find(int goalId);
int getNextId();
Future<List<Goal>> getGoalsList();
}
10 changes: 8 additions & 2 deletions lib/Services/Navigation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import 'package:flutter/cupertino.dart';
import 'package:goalkeeper/Models/Goal.dart';
import 'package:goalkeeper/Pages/CreateGoal.dart';
import 'package:goalkeeper/Pages/EditGoal.dart';
import 'package:goalkeeper/Services/Factory.dart';

void goToCreateGoal(context) async {
await Navigator.push(
context,
CupertinoPageRoute(
builder: (context) {
return CreateGoal();
return CreateGoal(
repository: Factory().repository,
notificationCenter: Factory().notificationCenter,
);
},
),
);
Expand All @@ -19,7 +23,9 @@ void goToEditGoal(context, Goal goal) async {
context,
CupertinoPageRoute(
builder: (context) {
return EditGoal(goal);
return EditGoal(goal,
notificationCenter: Factory().notificationCenter,
repository: Factory().repository);
},
),
);
Expand Down
22 changes: 9 additions & 13 deletions lib/Services/NotificationCenter.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:goalkeeper/Models/Goal.dart';
import 'package:goalkeeper/Services/GoalsRepository.dart';
import 'package:goalkeeper/Services/Interfaces/IRepository.dart';

class NotificationCenter {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
new FlutterLocalNotificationsPlugin();

List<PendingNotificationRequest> _pendingNotificationsCache;
GoalsRepository repo = GoalsRepository();
final IRepository repository;

static final NotificationCenter instance = NotificationCenter.getInstance();
factory NotificationCenter() => instance;

NotificationCenter.getInstance() {
NotificationCenter({@required this.repository}) {
var initNotificationSettings = getInitSettings();
flutterLocalNotificationsPlugin.initialize(initNotificationSettings,
onSelectNotification: defaultCallBack);
}

Future<bool> defaultCallBack(String goalId) {
if (goalId != null) {
Goal goal = repo.find(int.parse(goalId));
Goal goal = repository.find(int.parse(goalId));
// still need to figure how to go to EditPage from here !
print('got this notification: $goal');
}
Expand Down Expand Up @@ -71,10 +68,9 @@ class NotificationCenter {
}

Future<bool> goalHasNotification(Goal goal) async {
if (_pendingNotificationsCache == null)
_pendingNotificationsCache =
await flutterLocalNotificationsPlugin.pendingNotificationRequests();
return _pendingNotificationsCache
var pendingNotifications =
await flutterLocalNotificationsPlugin.pendingNotificationRequests();
return pendingNotifications
.any((notification) => notification.id == goal.id);
}

Expand All @@ -83,7 +79,7 @@ class NotificationCenter {
if (hasNotification) {
flutterLocalNotificationsPlugin.cancel(goal.id);
}
// if there is notification then update, and insert
// if there is notification then update, and insert it anyway
scheduleNotification(goal);
}
}
26 changes: 24 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:flutter/material.dart';
import 'package:goalkeeper/Pages/Home.dart';
import 'package:goalkeeper/Services/Db.dart';
import 'package:goalkeeper/Services/Factory.dart';
import 'package:goalkeeper/Services/GoalsRepository.dart';
import 'package:goalkeeper/Services/Interfaces/IRepository.dart';
import 'package:goalkeeper/Services/NotificationCenter.dart';
import 'package:goalkeeper/Utils/colors.dart';

void main() => runApp(MyApp());
void main() {
prepareDependencies();
runApp(MyApp(
repository: Factory().repository,
));
}

// Composition root for our dependencies !
void prepareDependencies() {
var factory = Factory();
factory.registerDatabase(Db());
factory.registerRepository(GoalsRepository(database: factory.database));
factory.registerNotificationCenter(
NotificationCenter(repository: factory.repository));
}

class MyApp extends StatelessWidget {
final IRepository repository;
MyApp({@required this.repository});

@override
Widget build(BuildContext context) {
return DynamicTheme(
Expand All @@ -19,7 +41,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: "Goalkeeper",
theme: theme,
home: Home(),
home: Home(repository: this.repository),
);
},
);
Expand Down
24 changes: 17 additions & 7 deletions lib/pages/CreateGoal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,40 @@ import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:goalkeeper/Models/Goal.dart';
import 'package:goalkeeper/Services/GoalsRepository.dart';
import 'package:goalkeeper/Services/Interfaces/IRepository.dart';
import 'package:goalkeeper/Services/NotificationCenter.dart';
import 'package:goalkeeper/Utils/HelperUtils.dart';
import 'package:goalkeeper/Utils/ThemeUtils.dart';
import "package:goalkeeper/Utils/colors.dart";
import "package:goalkeeper/Utils/pickers.dart";

class CreateGoal extends StatefulWidget {
final IRepository repository;
final NotificationCenter notificationCenter;

CreateGoal({@required this.repository, @required this.notificationCenter});

@override
State<StatefulWidget> createState() {
return CreateGoalState();
return CreateGoalState(
repository: this.repository,
notificationCenter: this.notificationCenter);
}
}

class CreateGoalState extends State<CreateGoal> {
GoalsRepository repo = GoalsRepository();
final IRepository repository;
final NotificationCenter notificationCenter;

Goal goal = new Goal("", "");
Color invertColor;

FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
new FlutterLocalNotificationsPlugin();

CreateGoalState(
{@required this.repository, @required this.notificationCenter});

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -195,12 +206,11 @@ class CreateGoalState extends State<CreateGoal> {
Navigator.pop(context);
if (goal.title.trim().isNotEmpty) {
if (goal.id == null) {
repo.insert(goal);
repository.insert(goal);
} else {
repo.update(goal);
repository.update(goal);
}
var noty = NotificationCenter();
noty.scheduleNotification(goal);
notificationCenter.scheduleNotification(goal);
}
}

Expand Down