I've been building Affirmi, a mood-based affirmation app.
The idea behind Affirmi is pretty simple:
Your affirmation should have something to do with how you're actually feeling.
A lot of affirmation apps take the same approach. You open the app, press a button, and get a random positive sentence.
That's not really what I wanted to build.
If I'm having a rough day, I don't necessarily want "You are capable of anything!" thrown at me at random. I want something that actually acknowledges where I am right now.
So Affirmi uses your mood and other context to generate affirmations that are more relevant to your current state.
For the app itself, I chose Kotlin Multiplatform. It lets me share a significant amount of code between Android and iOS while still allowing each platform to use its native APIs where it makes sense.
And that last part became particularly important this week.
I started working on one of those features that sounds simple until you actually build it:
Putting Affirmi's affirmations on the home screen.
Why widgets?
There's an interesting problem with apps like Affirmi.
I don't actually want someone to open the app every time they need an affirmation.
If the goal is to provide a small moment of encouragement during someone's day, making them:
unlock phone → find app → open app → wait → read affirmation
is already too much friction.
A widget changes that.
Instead, the affirmation can simply be there when you pick up your phone.
That makes widgets one of those features that looks like a small UI addition but can actually change how the product is used.
And then I started implementing it.
That's when I discovered something I probably should have expected:
Android and iOS have very different ideas about what a widget actually is.
The first lesson: a widget isn't just another screen
Coming from a normal application architecture, it's tempting to think about a widget like this:
App
└── Widget Screen
└── ViewModel
└── Repository
└── API
Unfortunately, that's not really how widgets work.
A widget exists in a system-managed environment. Your application isn't guaranteed to be alive when the widget needs to display something.
That changes the architecture.
The important question isn't:
"How do I reuse my existing screen?"
It's:
"What's the smallest piece of data the widget needs, and how do I make sure that data is available when my application isn't running?"
That question ended up leading to two different implementations.
Android: Glance + WorkManager
On Android, I used Jetpack Glance.
Glance is Google's framework for building app widgets using concepts familiar to Jetpack Compose.
That was particularly attractive for Affirmi because the rest of my Android UI already uses Compose.
But there's an important distinction:
Glance isn't simply Compose running inside a widget.
Glance ultimately produces the representation Android needs for an App Widget rather than giving you a normal Compose UI hierarchy that behaves like an application screen.
That means you have to think differently about state and lifecycle.
A widget can be removed, recreated, restored, or updated independently of whatever is happening in your main application.
So I didn't want the widget to become another place where I had to recreate Affirmi's entire application architecture.
Instead, I gave it a very small data contract.
Something conceptually like:
WidgetAffirmationPayload
- affirmation
- mood
- timestamp
- isPrivate
The widget doesn't need to know how an affirmation was generated.
It doesn't need to know about the recommendation algorithm.
It doesn't need to know about the user's mood history.
It just needs to know:
"What should I display right now?"
That separation turned out to be extremely useful.
So when does the widget update?
This was the next problem.
I wanted two kinds of updates.
1. User-triggered updates
If I open Affirmi and log a new mood, the widget should ideally reflect that change immediately.
So after generating new content, I trigger a widget update.
Conceptually:
User logs mood
↓
Affirmi generates affirmation
↓
Save latest affirmation
↓
Update widget
2. Background updates
But what happens if the user doesn't open Affirmi?
I still want the widget to have an opportunity to refresh.
That's where WorkManager comes in.
I use periodic background work as a baseline refresh mechanism, with network constraints so that the worker doesn't unnecessarily attempt network operations when connectivity isn't available.
And there's an important fallback:
If the network isn't available, the widget uses the last cached affirmation.
That's a small detail, but I think it's an important one.
For this type of product, stale content is usually better than no content.
I'd much rather have:
"You don't have to have everything figured out today."
from earlier in the day than a widget that suddenly says:
"Unable to load affirmation."
iOS: this is where things get interesting
The iOS implementation forced me to rethink the architecture.
Apple's WidgetKit widgets run as extensions rather than simply being another screen inside the application.
And this matters a lot for Kotlin Multiplatform.
My main iOS application has access to the shared KMP code.
The WidgetKit extension doesn't simply get to instantiate my normal KMP application stack and start calling repositories.
So instead of trying to force the widget into my existing architecture, I decided to treat it as what it really is:
a very small consumer of shared data.
That led me to App Groups.
App Groups became the bridge
The basic architecture looks like this:
Affirmi iOS App
│
│ generates
│ affirmation
↓
Shared App Group container
│
│ reads
↓
Widget Extension
│
↓
WidgetKit
The main application writes a serialized WidgetAffirmationPayload into the shared App Group container.
The widget extension then reads that payload.
The widget doesn't need to run Affirmi's entire domain layer.
It doesn't need to make the network request itself.
It doesn't need to know how the recommendation engine works.
It just reads the latest data and renders it.
When the app updates the shared data, it can also ask WidgetKit to reload the relevant timeline.
That separation made the iOS architecture much cleaner.
Timeline-based thinking is different
WidgetKit also introduced another concept that took some getting used to:
timelines.
Instead of thinking:
"Refresh this widget every X minutes."
you give WidgetKit entries representing what the widget should show and a policy for when it should ask for more data.
That's a very different mental model from a normal application screen.
And importantly, iOS controls the actual execution.
You can request reloads, but you shouldn't build your architecture around the assumption that the operating system will execute your request at exactly the time you asked for.
This is one of those places where mobile development teaches you a useful lesson:
the OS is part of your runtime.
You don't completely control when background work happens.
You design around the guarantees the platform actually gives you.
Then I ran into a problem I hadn't considered
The affirmation itself.
Not the widget.
Not KMP.
Not WidgetKit.
The privacy implications of the content.
Affirmi isn't displaying weather.
It's not showing a stock price.
It's not showing a calendar appointment.
It's displaying something that can be directly related to how a person is feeling.
Imagine someone has logged that they're anxious, overwhelmed, lonely, or having a difficult day.
Now imagine that affirmation appearing on the lock screen.
Anyone standing next to them can potentially see it.
That's a very different privacy problem from most widget content.
The lock screen is not the home screen
This led me to make a deliberate product decision.
For home screen widgets, Affirmi can display the actual affirmation by default.
The phone is already unlocked, and the user has intentionally placed the widget on their home screen.
But for lock screen widgets, I wanted a safer default.
So the lock screen version can start in a masked state.
Something along the lines of:
Affirmi
Tap to view today's affirmation.
Instead of immediately displaying potentially personal text.
The user can explicitly choose to show the real affirmation if that's what they want.
This might seem like a tiny configuration detail.
I don't think it is.
For wellness, journaling, mental-health, or mood-based products, privacy shouldn't be a single global setting.
The surface matters.
A setting that makes sense for the unlocked home screen might not make sense for a lock screen.
That became one of my favourite lessons from building this feature.
What I would do differently if I started again
There are a few things I'd recommend to anyone building a similar feature.
1. Define the widget data contract first
Before touching the UI, define the smallest payload the widget needs.
For example:
WidgetAffirmationPayload
├── affirmation
├── mood
├── timestamp
└── privacy state
That gives both platforms a common conceptual model even though their implementations are completely different.
It also prevents the widget from becoming coupled to your application architecture.
2. Design for stale data
Don't assume the network will always be available.
A widget should have a useful cached state.
For Affirmi, the last successfully generated affirmation is valuable even when the phone is offline.
That's especially important for a product that is supposed to be available during small moments throughout the day.
3. Don't fight the operating system
Android and iOS give you different background execution models.
Trying to make them behave identically usually creates unnecessary complexity.
With KMP, I share the business logic and data models where it makes sense.
But when I reach something deeply platform-specific, I let Android be Android and iOS be iOS.
That's one of the things I like about Kotlin Multiplatform.
Shared code doesn't mean identical code.
It means sharing the code where sharing provides value.
4. Treat privacy as part of the architecture
Don't bolt privacy onto the widget after the UI is finished.
For something like Affirmi, the data itself can be sensitive.
So privacy needs to be part of the widget payload, storage strategy, and presentation logic from the beginning.
The architecture I ended up with
At a high level, the result looks something like this:
Affirmi
│
Shared KMP Layer
│
┌────────────┴────────────┐
│ │
Android iOS
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ │ │ │
Glance WorkManager App Group WidgetKit
│ │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
└──────── Widget Data ────┘
│
Latest affirmation
The interesting part is that the user sees essentially the same feature on both platforms.
Underneath, however, the implementation is quite different.
And that's okay.
What I like about this feature
I'm still early in building Affirmi, but this is exactly the kind of feature that makes the product feel more useful to me.
The goal isn't to make another app that people open once a day because a notification tells them to.
I'd rather Affirmi quietly become part of someone's day.
You unlock your phone.
You see an affirmation that actually relates to how you're feeling.
Maybe you spend five seconds reading it.
Maybe that's all you needed.
That's the product I'm trying to build.
And the widget is one small step toward making that happen.
Final thoughts
The biggest takeaway for me wasn't actually about Jetpack Glance or WidgetKit.
It was this:
When you're building cross-platform software, the things users experience as one feature don't necessarily need to be implemented as one architecture.
Kotlin Multiplatform gives me a great place to share the parts that genuinely benefit from being shared — domain logic, models, networking, repositories, and other application logic.
But the moment I cross into operating-system-specific territory, I need to respect the platform.
Android has its own widget lifecycle and background execution model.
iOS has WidgetKit, extensions, timelines, App Groups, and its own execution constraints.
Trying to hide all of that behind a fake "universal widget architecture" would probably make the code worse, not better.
The better approach is to share the intent, the data contract, and the business rules — then let each platform handle the last mile.
That's what I ended up doing with Affirmi.
And now, instead of opening the app to get an affirmation, you can put one right where you already look dozens of times a day:
your phone's home screen.
References
- Kotlin Multiplatform
- Jetpack Glance — Android Developers
- Manage and update GlanceAppWidget — Android Developers
- Create an app widget with Glance — Android Developers
- WorkManager — Android Developers
- WidgetKit — Apple Developer Documentation
- TimelineProvider — Apple Developer Documentation
- Timeline — Apple Developer Documentation
Building Affirmi
If you're interested in what I'm building with Kotlin Multiplatform, you can check out Affirmi.
The idea is simple: affirmations should meet you where you are, rather than giving everyone the same generic quote.
I'm documenting the engineering journey as I build it. From KMP architecture and recommendation systems to offline-first data, AI-generated affirmations, privacy, and now widgets.
Top comments (0)