In today’s fast-paced digital world, mobile apps have become an integral part of our daily lives. Whether you’re a seasoned web developer or a backend guru looking to expand your skillset, creating a cross-platform mobile app is an exciting venture. And what better way to dive into this world than by building a practical habit tracker app using Flutter?
Let’s embark on this journey together, shall we?
The Rise of Cross-Platform Development
Remember the days when developing for iOS and Android meant learning two completely different languages and frameworks? It was like being forced to write with both hands simultaneously – possible, but oh so challenging!
Enter cross-platform development, the superhero of the mobile app world. It swooped in, cape fluttering, promising to save developers from the perils of platform-specific coding. And leading the charge? None other than Flutter, Google’s open-source UI software development kit.
Why Flutter, you ask? Well, imagine if you could write code once and have it run smoothly on both iOS and Android devices. Sounds like a developer’s dream, right? That’s precisely what Flutter offers. It’s like having a universal remote for your TV, but instead, you’re controlling multiple mobile platforms with a single codebase. Pretty neat, huh?
But before we dive headfirst into the world of Flutter, let’s take a moment to appreciate the evolution of cross-platform development. It’s been quite a journey!
A Brief History of Cross-Platform Mobile Development
Cast your mind back to the early days of smartphones. We had the iPhone with its Objective-C (later Swift) and Android with Java. Developers were forced to choose sides or become bilingual coders. It was like the Jets and the Sharks of the coding world, minus the choreographed dance battles (although I’m sure some developers would have preferred that).
Then came the hybrid solutions. Remember PhoneGap and Cordova? They were like the well-meaning but slightly awkward middlemen, trying to bridge the gap between web and mobile. They did their best, bless them, but performance issues and a lack of native feel left many developers and users wanting more.
Next up was React Native, Facebook’s contribution to the cross-platform arena. It was a game-changer, allowing developers to use their beloved JavaScript to create native apps. It was like finding out your favorite coffee shop now also serves gourmet meals – exciting, but you knew there had to be a catch.
And then, in 2017, Flutter entered the scene. It was like the new kid in school who was good at everything – fast, flexible, and with a knack for making things look good. Flutter quickly gained popularity, and for good reason.
Why Flutter Stole Our Hearts
- Hot Reload: This feature is like having a magical undo button. Make changes to your code, hit save, and see the results instantly in your app. It’s so satisfying it should come with a warning label: “May cause excessive coding enthusiasm.”
- Beautiful UI Out of the Box: Flutter comes with a rich set of customizable widgets. It’s like having a fully stocked art supply store at your fingertips. Want a sleek, modern look? There’s a widget for that. Prefer something more playful? Widget. It’s widgets all the way down!
- Performance: Flutter compiles to native code, which means your app runs smoothly and quickly. It’s like switching from a bicycle to a sports car – suddenly, everything is faster and more exhilarating.
- Single Codebase: Write once, run anywhere. It’s the holy grail of cross-platform development, and Flutter delivers. It’s like being able to speak one language and have everyone understand you, no matter where you go.
- Strong Community Support: The Flutter community is vibrant and always ready to help. It’s like joining a club where everyone is excited about the same thing you are, and they’re all eager to share their knowledge.
Now that we’ve set the stage, let’s roll up our sleeves and start building our habit tracker app. Trust me, by the end of this, you’ll be fluttering through app development like a pro!
Setting Up Your Flutter Development Environment
Before we dive into coding, we need to set up our development environment. It’s like preparing your kitchen before cooking a gourmet meal – you want everything in place before you start.
- Install Flutter: Head over to the official Flutter website and download the Flutter SDK for your operating system. It’s like picking up your new tool set – exciting, but just the beginning!
- Set Up an IDE: While you can use any text editor, I recommend using either Android Studio or Visual Studio Code with the Flutter extension. It’s like choosing between a Swiss Army knife and a specialized tool – both will get the job done, but the right choice depends on your preferences.
- Install Emulators: To test your app, you’ll need emulators for iOS and Android. If you’re on a Mac, Xcode comes with iOS simulators. For Android, you can set up emulators through Android Studio. It’s like having a practice field where you can test your app before sending it out into the real world.
- Flutter Doctor: Run the ‘flutter doctor’ command in your terminal. This handy tool checks your setup and tells you if anything is missing. It’s like having a personal assistant who makes sure you haven’t forgotten anything important.
Creating Your First Flutter Project
Alright, with our environment set up, let’s create our habit tracker app. We’ll call it “HabitHero” – because who doesn’t want to be the hero of their own habits?
Open your terminal and run:
flutter create habit_hero
cd habit_hero
This creates a new Flutter project and navigates into its directory. It’s like laying the foundation for a house – not very exciting to look at yet, but essential for everything that follows.
Now, let’s open this project in your chosen IDE and take a look at the structure. You’ll see a lot of files and folders, but don’t worry, we’ll focus on the important ones.
The Heart of the App: lib/main.dart
This is where the magic happens. Open lib/main.dart and you’ll see some boilerplate code. Let’s replace it with the basic structure of our app:
import 'package:flutter/material.dart';
void main() {
runApp(HabitHero());
}
class HabitHero extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Habit Hero',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HabitList(),
);
}
}
class HabitList extends StatefulWidget {
@override
_HabitListState createState() => _HabitListState();
}
class _HabitListState extends State<HabitList> {
List<String> habits = ['Read for 30 minutes', 'Exercise', 'Meditate'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(habits[index]),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// We'll implement this later
},
child: Icon(Icons.add),
),
);
}
}
This code sets up the basic structure of our app. We have a main HabitHero widget that sets up the app’s theme and home page. The home page is a HabitList widget that displays our list of habits.
Let’s break it down:
- The main() function is the entry point of our app. It calls runApp() with our root widget, HabitHero.
- HabitHero is a StatelessWidget that sets up the overall app structure, including the theme and home page.
- HabitList is a StatefulWidget. We use a StatefulWidget because we’ll be updating the list of habits dynamically.
- In the _HabitListState, we have a list of habits and a build method that creates the UI for our list.
- We’re using a ListView.builder to efficiently create our list of habits. It’s like having a conveyor belt that only creates list items as they’re needed, rather than creating them all at once.
- We’ve added a FloatingActionButton that we’ll use later to add new habits.
Run Your App
Now for the moment of truth. In your terminal, run:
flutter run
Choose your preferred emulator, and voila! You should see your app running with a list of habits. It’s like watching your child take its first steps – simple, but full of potential!
Adding Interactivity: Creating New Habits
Our app looks nice, but it doesn’t do much yet. Let’s add the ability to create new habits. We’ll update our _HabitListState class:
class _HabitListState extends State<HabitList> {
List<String> habits = ['Read for 30 minutes', 'Exercise', 'Meditate'];
void _addHabit() {
setState(() {
habits.add('New Habit ${habits.length + 1}');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(habits[index]),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: Icon(Icons.add),
),
);
}
}
We’ve added an _addHabit method that adds a new habit to our list. The setState call is crucial here – it tells Flutter that our state has changed and the UI needs to be rebuilt.
Now, when you tap the floating action button, a new habit will be added to the list. It’s like watching your app come to life!
Making it Pretty: Styling Our App
Our app is functional, but it could use a bit of pizzazz. Let’s add some custom styling:
import 'package:flutter/material.dart';
void main() {
runApp(HabitHero());
}
class HabitHero extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Habit Hero',
theme: ThemeData(
primarySwatch: Colors.purple,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HabitList(),
);
}
}
class HabitList extends StatefulWidget {
@override
_HabitListState createState() => _HabitListState();
}
class _HabitListState extends State<HabitList> {
List<String> habits = ['Read for 30 minutes', 'Exercise', 'Meditate'];
void _addHabit() {
showDialog(
context: context,
builder: (BuildContext context) {
String newHabit = "";
return AlertDialog(
title: Text('Add a new habit'),
content: TextField(
onChanged: (value) {
newHabit = value;
},
),
actions: <Widget>[
TextButton(
child: Text('Add'),
onPressed: () {
setState(() {
habits.add(newHabit);
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(habits[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
habits.removeAt(index);
});
},
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: Icon(Icons.add),
),
);
}
}
We’ve made several improvements:
- We’ve changed the app’s primary color to purple. Because heroes wear purple, right?
- We’ve added a dialog for adding new habits. It’s like giving your users a friendly conversation rather than just shouting “New Habit!” at them.
- We’ve wrapped each ListTile in a Card widget for a more polished look. It’s like framing each habit in its own little picture frame.
- We’ve added a CircleAvatar as a leading widget to number our habits. It’s like giving each habit its own little badge of honor.
- We’ve added a delete button for each habit. Because sometimes, breaking a bad habit is as important as forming a good one!
Persistence: Saving Our Habits
Our app is looking good, but it has one major flaw – all our habits disappear when we close the app. Let’s fix that by adding persistence. We’ll use the shared_preferences package for this.
First, add shared_preferences to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.6
Then run:
flutter pub get
Now, let’s update our _HabitListState class:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _HabitListState extends State<HabitList> {
List<String> habits = [];
@override
void initState() {
super.initState();
_loadHabits();
}
_loadHabits() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
habits = (prefs.getStringList('habits') ?? []);
});
}
_saveHabits() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setStringList('habits', habits);
}
void _addHabit() {
showDialog(
context: context,
builder: (BuildContext context) {
String newHabit = "";
return AlertDialog(
title: Text('Add a new habit'),
content: TextField(
onChanged: (value) {
newHabit = value;
},
),
actions: <Widget>[
TextButton(
child: Text('Add'),
onPressed: () {
setState(() {
habits.add(newHabit);
_saveHabits();
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(habits[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
habits.removeAt(index);
_saveHabits();
});
},
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: Icon(Icons.add),
),
);
}
}
We’ve made several changes:
- We’ve added _loadHabits and _saveHabits methods to load and save our habits using SharedPreferences.
- We call _loadHabits in initState to load our habits when the app starts.
- We call _saveHabits whenever we add or remove a habit.
Now our habits will persist even when we close the app. It’s like giving our habits a comfy home to live in, even when we’re not looking!
Taking It Further: Advanced Features
Habit Streaks
One of the most motivating aspects of habit tracking is seeing your streak – the number of consecutive days you’ve completed a habit. Let’s add this feature to our app.
First, we’ll need to modify our habit data structure. Instead of just storing strings, we’ll create a Habit class:
class Habit {
String name;
int streak;
DateTime lastCompleted;
Habit({required this.name, this.streak = 0, DateTime? lastCompleted})
: lastCompleted = lastCompleted ?? DateTime.now();
Map<String, dynamic> toJson() => {
'name': name,
'streak': streak,
'lastCompleted': lastCompleted.toIso8601String(),
};
factory Habit.fromJson(Map<String, dynamic> json) => Habit(
name: json['name'],
streak: json['streak'],
lastCompleted: DateTime.parse(json['lastCompleted']),
);
}
Now, let’s update our _HabitListState to use this new Habit class:
class _HabitListState extends State<HabitList> {
List<Habit> habits = [];
void _loadHabits() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
habits = (prefs.getStringList('habits') ?? [])
.map((String habitJson) => Habit.fromJson(json.decode(habitJson)))
.toList();
});
}
void _saveHabits() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
'habits', habits.map((habit) => json.encode(habit.toJson())).toList());
}
void _completeHabit(int index) {
setState(() {
Habit habit = habits[index];
DateTime now = DateTime.now();
if (now.difference(habit.lastCompleted).inDays == 1) {
habit.streak++;
} else if (now.difference(habit.lastCompleted).inDays > 1) {
habit.streak = 1;
}
habit.lastCompleted = now;
_saveHabits();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text('${habits[index].streak}'),
),
title: Text(habits[index].name),
trailing: IconButton(
icon: Icon(Icons.check),
onPressed: () => _completeHabit(index),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: Icon(Icons.add),
),
);
}
}
We’ve made several changes here:
- We’re now storing and loading Habit objects instead of simple strings.
- We’ve added a _completeHabit method that updates the streak and last completed date of a habit.
- We’ve updated our UI to display the streak count and added a check button to mark habits as completed.
- Habit Categories
Categorize Habits
Let’s add the ability to categorize habits. This will help users organize their habits and make the app more visually appealing.
First, let’s update our Habit class:
enum Category { Health, Productivity, Relationships, Personal }
class Habit {
String name;
int streak;
DateTime lastCompleted;
Category category;
Habit({
required this.name,
this.streak = 0,
DateTime? lastCompleted,
required this.category,
}) : lastCompleted = lastCompleted ?? DateTime.now();
Map<String, dynamic> toJson() => {
'name': name,
'streak': streak,
'lastCompleted': lastCompleted.toIso8601String(),
'category': category.index,
};
factory Habit.fromJson(Map<String, dynamic> json) => Habit(
name: json['name'],
streak: json['streak'],
lastCompleted: DateTime.parse(json['lastCompleted']),
category: Category.values[json['category']],
);
}
Now, let’s update our UI to display habits grouped by category:
@override
Widget build(BuildContext context) {
Map<Category, List<Habit>> groupedHabits = groupBy(habits, (Habit h) => h.category);
return Scaffold(
appBar: AppBar(
title: Text('Habit Hero'),
),
body: ListView.builder(
itemCount: Category.values.length,
itemBuilder: (context, categoryIndex) {
Category category = Category.values[categoryIndex];
List<Habit> categoryHabits = groupedHabits[category] ?? [];
return ExpansionTile(
title: Text(category.toString().split('.').last),
children: categoryHabits.map((habit) => ListTile(
leading: CircleAvatar(
child: Text('${habit.streak}'),
),
title: Text(habit.name),
trailing: IconButton(
icon: Icon(Icons.check),
onPressed: () => _completeHabit(habits.indexOf(habit)),
),
)).toList(),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: Icon(Icons.add),
),
);
}
We’re using the groupBy function from the collection package here, so make sure to add it to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.6
collection: ^1.15.0
Notifications
To help users remember to complete their habits, let’s add daily notifications. We’ll use the flutter_local_notifications package for this.
Add it to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.6
collection: ^1.15.0
flutter_local_notifications: ^9.1.5
Now, let’s add a method to schedule notifications:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class _HabitListState extends State<HabitList> {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
@override
void initState() {
super.initState();
_initializeNotifications();
_loadHabits();
}
void _initializeNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('app_icon');
final IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings();
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
void _scheduleNotification() async {
var time = Time(20, 0, 0); // 8:00 PM
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'habit_hero', 'Habit Hero', 'Daily reminder for your habits');
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.showDailyAtTime(
0,
'Habit Hero',
'Don\'t forget to complete your habits!',
time,
platformChannelSpecifics);
}
// Call _scheduleNotification() when adding a new habit
}
This sets up a daily notification at 8:00 PM to remind users to complete their habits.
Conclusion
And there you have it! We’ve taken our simple habit tracking app and transformed it into a feature-rich, user-friendly application. We’ve implemented habit streaks to keep users motivated, added categories for better organization, and even set up notifications to remind users to stay on track.
But the journey doesn’t end here. There’s always room for improvement and new features. Here are some ideas for further enhancements:
- Data Visualization: Add charts to show habit completion rates over time.
- Cloud Sync: Allow users to sync their habits across devices.
- Social Features: Let users share their progress or compete with friends.
- Custom Reminders: Allow users to set custom reminder times for each habit.
- Habit Insights: Provide users with insights about their most and least successful habits.
Remember, the key to building great apps is to continually iterate and improve based on user feedback. So don’t be afraid to put your app out there and see how real users interact with it.
Flutter has made it incredibly easy to build beautiful, high-performance apps for both iOS and Android. By leveraging Flutter’s rich widget library and powerful features, we’ve been able to create a complex app with relatively little code.
As you continue your Flutter journey, remember that the Flutter and Dart documentation are your best friends. Don’t hesitate to explore the vast ecosystem of Flutter packages available on pub.dev. And most importantly, keep coding, keep learning, and keep pushing the boundaries of what you can create!
Wrapping Up
And there you have it, folks! We’ve built a fully functional, cross-platform habit tracking app using Flutter. We’ve covered setting up a Flutter development environment, creating a basic app structure, adding interactivity, styling our app, and even implementing data persistence.
Happy Fluttering, and may your habits be ever in your favor!
Learn More: Flutter Mastery: Building Cross-Platform Mobile Apps | Udemy