
For this project, I created a pomodoro application for mobile and desktop! It is made with Flutter — this makes building to multiple devices easy! Operating systems on mobile devices such as Android and iOS and desktop devices such as Windows, Linux, and MacOS are all compatible!
Purpose
I was originally inspired by another pomodoro app which I used extensively “Minimalist Pomodoro Timer” by Goodtime. I loved the minimal aesthetic and how the developer made its code open-source as can be found on their Github. I use it almost every single day and it helps me stay focused.
However, it was missing some features that I wanted to have — namely a desktop app and cloud syncing between devices. Since I primarily use Linux OS and Android OS, Flutter is the perfect choice! Furthermore, AWS Amplify
Creating the timer
The timer requires stacking a text widget inside a CircularProgressIndicator widget. Both are stateful widgets that are updated together whenever the timer ticks every second.
void initializeTimer() {
setState(() {
_start = (_minute * 60) + _second;
_initial = _start;
_initialActual = _initial;
_initialMinute = _minute;
_initialSecond = _second;
});
}
void togglePlayPause() {
// if timer is currently running, pause it
if (_isPlaying) {
setState(() {
_isPlaying = false;
});
}
// run if timer is at initial time
else if (_start == _initial && _isPlaying == false) {
setState(() {
_isPlaying = true;
});
startTimer();
}
// run if timer is paused
else if (_start != 0 && _isPlaying == false) {
setState(() {
_isPlaying = true;
});
startTimer();
}
}
void tickTimer(Timer timer) {
if (_start == 0 || _isPlaying == false) {
setState(() {
timer.cancel();
_isPlaying = false;
});
} else {
setState(() {
_start--;
_minute = _start ~/ 60;
_second = _start % 60;
});
}
}
void startTimer() {
const oneSec = Duration(seconds: 1);
_timer = Timer.periodic(
oneSec,
tickTimer,
);
}
void resetTime() {
if (_isBreak) {
setState(() {
_start = _break * 60;
_initial = _break * 60;
_minute = _break;
_second = 0;
});
} else {
setState(() {
_start = _initialActual;
_initial = _initialActual;
_minute = _initialMinute;
_second = _initialSecond;
});
}
}
...
Stack(
children: <Widget>[
ProgressIndicatorCircle(timeValue: (_start / _initial).toDouble()),
SizedBox(
width: 200,
height: 200,
child: Center(
// use padleft 3 to account for colon character
child: StyledTimer('$_minute:'.padLeft(3, '0') + '$_second'.padLeft(2, '0')),
),
),
],
),
...
TextButton(
onPressed: togglePlayPause,
style: TextButton.styleFrom(
backgroundColor: AppColors.secondaryColor,
),
child: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: AppColors.textColor,
size: 16.0,
),
),Using AWS Amplify for cloud syncing
First I needed to configure my application to use amplify
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await _configureAmplify();
runApp(
MaterialApp(
title: "Pomodoro",
theme: primaryTheme,
home: const Home()
)
);
}
Future<void> _configureAmplify() async {
// To be filled in
try {
// Create the API plugin.
//
// If `ModelProvider.instance` is not available, try running
// `amplify codegen models` from the root of your project.
final api = AmplifyAPI(modelProvider: ModelProvider.instance);
// Create the Auth plugin.
final auth = AmplifyAuthCognito();
// Add the plugins and configure Amplify for your app.
await Amplify.addPlugins([api, auth]);
await Amplify.configure(amplifyconfig);
safePrint('Successfully configured');
} on Exception catch (e) {
safePrint('Error configuring Amplify: $e');
}
}Creating the login
Next, the user needs to login so that their data will be saved to their particular profile. Luckily, flutter has a prebuilt sign in/sign up experience for Amplify Auth by importing their package. I included it in my widget builder.
Widget authenticationPage() {
return Authenticator(
child: MaterialApp(
theme: primaryTheme,
builder: Authenticator.builder(),
home: Container(
padding: const EdgeInsets.all(30.0),
child: Center(
child: Column(
children: [
TextButton(
onPressed: homeView,
style: TextButton.styleFrom(
backgroundColor: AppColors.secondaryColor,
),
child: Icon(
Icons.arrow_back,
color: AppColors.textColor,
size: 16.0,
),
),
const SizedBox(height: 20.0),
const StyledText('You are logged in!'),
const SizedBox(height: 20.0),
TextButton(
onPressed: signOut,
style: TextButton.styleFrom(
backgroundColor: AppColors.secondaryColor,
),
child: Icon(
Icons.logout,
color: AppColors.textColor,
size: 16.0,
),
),
],
),
),
),
),
);
}I created a user “bob” and verified the email.

Afterwards, I can login with the user.

Looking at my Cognito User pool, I checked that the user exists and the email is verified.

Creating the activities
Here the user can input their own data to create an activity. They can specify the name of the activity along with the time and break time.

Once submitted, the activity should show up as a card in a scrollable list.

The user then can choose a card to be loaded as an active pomodoro on the home page.
class PomodoroActivity {
// constructor
PomodoroActivity({
required this.activity, required this.active_timer, required this.break_timer
});
// fields
final String activity;
final int active_timer;
final int break_timer;
bool _isActive = false;
// getters
get isActive => _isActive;
// methods
void activePomodoro() {
_isActive = !_isActive;
}
void updateActiveTimer(int value) {
active_timer = value;
}
void updateBreakTimer(int value) {
break_timer = value;
}
}Syncing the data with the cloud
Here I use the following schema for the activity entry.
type ActivityEntry @model @auth(rules: [{ allow: owner }]) {
id: ID!
title: String!
activeTimer: Int!
breakTimer: Int
}Then I capture the user’s input for the activity in a form and make an Amplify POST request.
Future<void> submitForm() async {
// If the form is valid, submit the data
final curActivity = _activityController.text;
final curTime = int.parse(_timeController.text);
final curBreak = int.parse(_breakController.text);
// Create a new budget entry
final newEntry = ActivityEntry(
title: curActivity,
activeTimer: curTime,
breakTimer: curBreak,
);
final request = ModelMutations.create(newEntry);
final response = await Amplify.API.mutate(request: request).response;
safePrint('Create result: $response');
await _refreshActivityEntries();
}This gets reflected in my DynamoDB database

If I were to open another device with the same user login. The same cards should also appear in the activity view screen.
Here I opened my application in a desktop app, login with my user, and the same cards appear! It pulled the data from DynamoDB!

Deleting the activity
The user can optionally delete an activity they do not want. This will also delete the activity on AWS DynamoDB.
Future<void> _deleteActivityEntry() async {
if (_activityEntries.isEmpty) {
return;
}
ActivityEntry activityEntry = _activityEntries[_activityEntries.length - 1];
final request = ModelMutations.delete<ActivityEntry>(activityEntry);
final response = await Amplify.API.mutate(request: request).response;
safePrint('Delete response: $response');
await _refreshActivityEntries();
}