
Projects · · 6 min read
Why I Built My Own Japanese Learning App
- Flutter
- Case Study
- UI/UX
- Productivity
There are dozens of polished Japanese learning apps. I still built my own. Not because I thought I'd beat them, but because I wanted an app that matched how I actually study, and because building it taught me more than any tutorial could.
The app is called Kanjiro. It's a Flutter app focused on the first steps of Japanese: hiragana, flashcards, daily streaks, and everything working offline. It's a personal project, built in the gaps between client work and leading projects, and it's been one of the most useful things I've made, both as a learner and as a developer.
The itch
When I started learning Japanese, I tried the usual apps. They were good at many things. They were also full of things I didn't want: aggressive notifications, features locked behind subscriptions, gamified screens that felt more like a slot machine than a study tool, and lessons that assumed a steady internet connection.
What I wanted was simple:
- Open the app and be practising within two seconds.
- Drill hiragana until I stopped hesitating.
- See a streak that rewards showing up, without guilt-tripping me.
- Study on a bus, in a power cut, or with mobile data switched off.
No existing app hit all four in the way I wanted. And as a developer, "I can't find the tool I want" is a dangerous sentence, because the next thought is "I could build that."
Why Flutter
My day-to-day work is mostly React and Next.js, so a web app would have been the obvious choice. I picked Flutter on purpose.
- It's a phone-first habit. Studying happens in small moments, on a phone. A native app on the home screen gets opened. A browser tab gets forgotten.
- Offline is natural. A mobile app with local storage works offline by default. On the web, I'd have to build that with a service worker and careful caching.
- One codebase, both platforms. I wanted Android and iOS without maintaining two apps.
- I wanted to learn it properly. Reading Flutter docs isn't the same as shipping something with it.
I mentioned in Why I Use Next.js for Most of My Modern Web Projects that mobile apps are one place I don't reach for Next.js. Kanjiro is the clearest example.
Offline-first, not offline-tolerant
There's a real difference between an app that survives losing the network and one that never needed it. I built Kanjiro as the second kind.
All the study content, the kana and the flashcard decks, ships with the app. Progress, review history and streaks are stored on the device. There's no login screen standing between you and practice, and no loading spinner when you open a deck. The network isn't in the critical path at all.
This came from the same thinking I apply to client work in Nepal, where connections drop and data costs money. I wrote about that in Designing Apps for Slow Internet Connections. For a personal study app, the logic is even stronger. If I can't practise on a bus because a server is slow, the app has failed at its only job.
The best offline feature is a design where the network was never required in the first place.
Hiragana first, and properly
Many apps rush through the kana so you can get to "real" lessons. I think that's backwards. If you hesitate on every character, every later lesson is slower and more frustrating.
So Kanjiro starts with hiragana and stays there until it's automatic. The drill shows a character, asks for the reading, and tracks which characters you get wrong. Confusable pairs, like the ones that differ by a single stroke, come back more often. It's not complicated, but it's targeted at exactly where I was struggling.
Flashcards that resurface what you forget
The flashcards use a simple spaced-repetition idea: cards you know well come back less often, and cards you miss come back soon. I didn't try to reinvent a research-grade algorithm. A small, understandable rule set was enough for me, and easier to debug.
class CardReview {
final int intervalDays;
final double ease;
const CardReview({this.intervalDays = 0, this.ease = 2.5});
CardReview next({required bool correct}) {
if (!correct) {
// Missed: see it again tomorrow and make it a little harder.
return CardReview(intervalDays: 1, ease: (ease - 0.2).clamp(1.3, 3.0));
}
final nextInterval = intervalDays == 0 ? 1 : (intervalDays * ease).round();
return CardReview(intervalDays: nextInterval, ease: ease);
}
}Keeping the logic in plain Dart classes, separate from widgets, meant I could test it without touching the UI. That habit came straight from my web work, where I keep business rules out of components.
Streaks without guilt
Streaks are powerful and easy to abuse. Plenty of apps use them to make you feel bad. I wanted mine to feel like a quiet record of effort.
The rules I settled on:
- A day counts if you complete any session, even a short one.
- The streak is shown, but there are no alarming red warnings when it's at risk.
- Days are counted by the phone's local date, so a late-night session counts for the day you actually studied.
The date logic is the part that's easy to get wrong. Here's the core of it:
int currentStreak(List<DateTime> sessionDates, DateTime now) {
final days = sessionDates
.map((d) => DateTime(d.year, d.month, d.day))
.toSet();
var day = DateTime(now.year, now.month, now.day);
// If today has no session yet, the streak can still be alive from yesterday.
if (!days.contains(day)) day = day.subtract(const Duration(days: 1));
var streak = 0;
while (days.contains(day)) {
streak++;
day = day.subtract(const Duration(days: 1));
}
return streak;
}That "still alive from yesterday" check matters. Without it, the streak shows zero every morning until you study, which feels like a punishment for waking up.
Designing it like a client project
I'm a UI/UX designer first, so I started Kanjiro in Figma, not in code. I treated myself as the client and wrote a short brief: who it's for, what a session looks like, what success feels like.
A few design decisions that came out of that:
- One primary action per screen. The home screen exists to start a session. Everything else is secondary.
- Large characters, generous spacing. You're reading unfamiliar scripts. Cramped layouts make that harder.
- Calm colour. No flashing rewards. A subtle confirmation when you get something right is enough.
- Short sessions by default, so starting never feels like a commitment.
What building it taught me
Kanjiro made me a better developer in ways client work sometimes doesn't, because I was responsible for everything: product, design, code and the consequences.
- Scope is personal too. I had a long list of features I wanted. Shipping a small app I actually use beat planning a big one I never finished.
- Local state still needs structure. Offline-first doesn't mean simple. Data models and migrations matter even when nothing touches a server.
- Being your own user is honest feedback. When a screen annoyed me on the bus, I fixed it that evening.
- Flutter rewards a clean separation between logic and widgets, the same way React does.
Where I've landed
Building your own version of an app that already exists can look like wasted effort. For me it was the opposite. Kanjiro is the study tool I wanted, and building it sharpened skills I use every day: designing for offline, keeping logic testable, and saying no to features.
If there's a tool you keep wishing existed, try building the smallest version of it for yourself. You'll learn what's actually hard about it, and you might end up using it every day.
