Daily step counter in Flutter
On the time of writing, I'm working on a Flutter game with which I try to motivate the player to walk daily. By walking they will earn resources that they'll be able to use in the game. The more they'll walk, the more resources they can earn.
I need a way to track the step count for each day, as the progression should reset at midnight.
The tricky requirement
Just tracking the steps might seem easy, but while building this, I ran into a case that made it a bit more tricky. At least I needed some time to get to a proper solution.
Because the app should show the amount of steps from the current day, I need to keep track of the current day. So when the user is actively using the app and it becomes midnight, thus a new day starts, the app should reflect this so the player is not longer able to claim rewards from the previous day.
Pedometer 2
I started looking on pub.dev for a plugin that I could use to track steps. There are quite some plugins build by the community and most of them seem to be forks of each other, which made it really hard to find the 'right' one.
Eventually I went for pedometer_2, simply because I liked the documentation best.
Platform differences
It turned out that both platforms that I wish to support (iOS and Android), do not have the same capabilities.
- Get real time step count since a date (Stream) (Only on IOS)
*Alternative for Android explained under
Getting real time step count since a certain date (start of today) sounded exactly what I needed, luckily there is an explanation for an alternative on Android!
Permissions
To get access to the step counter information you need to get permissions from the user. I will not cover it in this article as the documentation explains it very well here.
Creating the listener
Ideally we would want to use the .stepCountStreamFrom(...)
method because it allows us to specify the period. But as the documentation clearly states, this is not supported on Android.
Luckily, the documentation mentions an alternative solution.
- (Android alternative)*Use a combination of the*
getStepCount
andstepCountStream
. Example in theExample App
The .getStepCount
used in the example app, supports a from
and to
argument, on both Android and iOS.
The .stepCountStream
used in the example streams the amount of steps registered since the last system boot. When the user is actively walking, the stream will continuously emit new values.
By combining the two, and sprinkling a bit of rxdart magic, we can create the stream that we need.
final stepCounterService = StepCounterService();
class StepCounterService {
StepCounterService() {
// We listen to stepCountStream so we get new data
// when the user is walking.
// But we don't use the value as it gives us the total
// steps since last device boot
Pedometer().stepCountStream.listen((totalSteps) async {
// Now we determine the periode for which we wish to
// get the amount of steps, which is today.
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = startOfDay.add(Duration(days: 1));
// Now we use this information to use the getStepCount
// method to get the steps for today.
final stepCount = Pedometer().getStepCount(
from: startOfDay,
end: endOfDay,
);
// And then we add this stepCount to the
// _stepCounterController.
_stepCounterController.add(stepCount);
});
}
// We use a BehaviorSubject because it holds the last value
// and emits it whenever a new listener subscribes.
// This makes a Stream more useful to be used in the UI.
final _stepCounterController = BehaviorSubject.seeded(0);
final Stream<int> get todaysStepCount => _stepCounterController;
}
Great, we now get the amount of steps for today which is updated whenever the player walks.
However...
It's a new dawn, it's a new day
In the above code, we get the step count of today. But what if it is no longer today?
Our service initializes the stream when the app starts. It uses the current day, but when we reach midnight, we want the step count of the next day.
In other words, the periode should be dynamic.
More streams!
The rxdart package has some very useful utilities that we can use.
Let's add a dynamic way of managing the date to our service.
final stepCounterService = StepCounterService();
class StepCounterService {
StepCounterService() {
// We use CombineLatestStream, also from rxdart, to combine
// two streams. The listener will be called whenever
// one of the streams changes.
CombineLatestStream.combine2(
Pedometer().stepCountStream(),
_todayController,
(totalSteps, date) => date, // We only need the date
).listen((date) async {
final startOfDay = DateTime(date.year, date.month, date.day);
final endOfDay = startOfDay.add(Duration(days: 1));
final stepCount = Pedometer().getStepCount(
from: startOfDay,
end: endOfDay,
);
_stepCounterController.add(stepCount);
});
}
// We can use this to force the date to update.
void updateDate() => _todayController.add(DateTime.now());
final _stepCounterController = BehaviorSubject.seeded(0);
final Stream<int> get todaysStepCount => _stepCounterController;
// We use this to keep track of the day
final _todayController = BehaviorSubject.seeded(DateTime.now());
}
We've now made the date for which the step count is requested, dynamic. By using stepCounterService.updateDate()
, we're able to force the service to use the current date.
Updating the date
Now that we have a way to update the date, we need to determine when to call it.
When the app is opened, it will already initialize with the current date. So we should call our method when we reach the next day. For this we could use a background task, or even easier, a Timer
.
void main() {
// Schedule the first
_scheduleUpdate();
}
void _scheduleUpdate() {
// Force the service to update the date.
stepCounterService.forceUpdate();
// Calculate how long it takes till tomorrow.
final now = DateTime.now();
final tomorrow = now.add(Duration(days: 1));
final startOfTomorrow = DateTime(
tomorrow.year,
tomorrow.month,
tomorrow.day,
);
final tillMidnight = now.difference(startOfTomorrow);
// Schedule this function again at the start of tomorrow.
Timer(tillMidnight, _scheduleUpdate);
}
Now the _scheduleUpdate
function will be called once at app startup, and then it will schedule itself again for every next midnight.
Conclusion
With some rxdart magic and some well timed (pun intended) function calls, we're able to overcome the platform limitations on Android and are able to get the daily step count as a stream.
Subscribe to my newsletter
Read articles from Stephan E.G. Veenstra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Stephan E.G. Veenstra
Stephan E.G. Veenstra
Stephan is a Flutter Developer from The Netherlands. By day he is working on serious apps for Pinch, by night he likes to play around by making games and writing about what he has learned.