Tonight

It decides what you cook. It doesn't tell you how.

Google Play: https://play.google.com/store/apps/details?id=com.apn201.tonight

Entered for the Nutrition & Healthy Eating award (Abbey's Kitchen), the RevenueCat Design Award and the RevenueCat Peace Prize.

There is no paywall and no trial. Everything works from the first launch, so judges need no promo code for anything. The RevenueCat purchase is a tip that unlocks nothing.

Inspiration

I have had trouble with weight management for about 20 years, and I have been planning software for it most of that time. The plans were always too big and nothing ever shipped. Shipaton made me take one piece out of them and finish it.

The piece is one question. What should we cook tonight.

I know what is healthy and what is not, I don't need an app to teach me that. What I don't have is time to plan cooking every day, and that is what pushes you into the easy and bad decisions. By six o'clock you are tired and the freezer wins.

I also don't want an app commenting on my choices. Not a health app and not a weight loss app. So this is an inspiration app. It inspires you to eat better and to cook things you have never made, with no configuration and no questionnaire. You open it and it just knows.

What it does

One dish on a card. Swipe left for not tonight, right to cook it. That is the whole interaction.

It learns from what you turn down. No onboarding, no "tell us about your diet", no checklist of restrictions. Say no to enough pork and it notices they were all pork, then asks once, in plain words, whether to take it off the list. It never needs to know if the reason is a diet, an allergy, a religion or that you just don't like it, so it doesn't ask.

Every now and then it suggests something it expects you to refuse, to check it still had you right. An app that only narrows gets slowly wrong and never finds out about it. When the probe is accepted it says so and puts the thing back.

One tap opens a recipe search in the browser. There are no recipes in the app. The web does that better and in more languages than I could.

Accepting a card counts as intent and going on to ask for the recipe counts as evidence, and they are weighted differently (one mis-tap should not carry the weight of a meal).

The plate

This is the part I built for the Nutrition & Healthy Eating award.

When you accept a main, the app checks if it will actually fill you up. If it won't, it offers something to go alongside. It says it as dinner, not as nutrition:

That's a bit thin on its own. Something with it?

I wrote "balance your plate" as the prompt first and then cut it. Balance is a standard your dinner either met or didn't, and that makes the app the judge of your meal. The prompts ask how full you will feel instead, which is a question only the person eating can answer.

Add something and it asks again only if a real gap is left. When the plate is covered it stops talking. That was the hardest part to get right, although hardest here means deleting suggestions that worked fine. An app that always has one more idea is telling you that you haven't done enough.

It only adds. There is no code anywhere in the app for removing, reducing, substituting or warning. The only question it can ask is what to add.

The model is protein, fibre and fat, three flags on every dish, and a main missing one of them is where the offer fires. Same three as the Hunger Crushing Combo. I got there from the other direction, as the simplest honest answer to "will this fill me up", and noticed the match later.

No numbers anywhere. No calories, macros, grams, scores, percentages or streaks, about the food or about you. The plate is covered or it is not, and it says which in words.

What it offers relates to what you are making. Covering the gap comes first, but a side from the same part of the world scores higher than one from the other side of it, and the three offers are sampled rather than ranked, so two plates in a row don't look the same.

Taking it back

I accepted a shellfish dish by accident one evening of testing, the app believed something untrue about me, and there was no way at all to take it back. So everything it decides can now be seen and reversed.

"What I think about you" lists every lean it has worked out, in plain words, with a slider on each one. "What you've cooked" is a dinner history, and anything on it can be forgotten, which takes it out of what the app learned. The reinforcement is unwound by arithmetic and the evidence behind it is deleted, not hidden. Anything it has taken off the list is shown with one tap to put it back, and putting something back does not stop it noticing that pattern again later. Whole cuisines can be switched off by hand, for the things you already know about yourself and should not have to teach one card at a time.

What is not in it

No calorie counting, macro tracking, logging, streaks, targets or weekly reports. The app has no opinion about what you eat and never comments on it.

No photographs. Names on paper. It is a decision, not a menu, and food styling sets a standard a Tuesday cannot meet (it would also have meant 2,575 images).

No account, no sign-up, no cloud. Everything it learns stays on the phone. No paywall, so everything works from first launch. No notifications, so it never asks you to come back.

It works with no signal at all, right up until you ask it for a recipe. The database ships inside the app.

How I built it

Kotlin and Jetpack Compose, two modules. The :engine module is plain Kotlin/JVM with no Android in it, so the learner, the plate, the probes and the scoring all run under unit test without an emulator. 62 tests, all logic, no UI mocking.

The learner is a contingency table, not a model. For each facet it keeps a 2x2 table of shown against refused and computes a Wilson score lower bound. Three bad swipes on a Tuesday cannot reshape the app, twelve consistent ones can. Greedy explaining-away and a specificity rule stop a pork habit turning into "no meat".

Similarity is Gower, not cosine. The facets are categories with no order, and cosine over one-hot vectors quietly claims beef is nearer chicken than fish is. A weighted per-facet mean claims nothing of the sort.

The data comes from a python pipeline that runs at build time and produces a sqlite file, shipped in assets and opened with Room. 2,575 dishes, 24 culinary regions, 1,484 of them mains. Every field is checked against a controlled vocabulary at import, so a bad tag fails at build time instead of quietly at runtime.

RevenueCat

A tip jar, that is all.

The app is free. No premium tier, no trial, and no entitlement checking anywhere in the project, because there is nothing to gate. RevenueCat powers one consumable in-app purchase that unlocks nothing.

It sits at the bottom of the About screen, behind the drawer, under three paragraphs. It never interrupts, never shows up over a card and never fires on a timer. After somebody tips, the line turns into a thank you and the button stays (refusing a second tip from someone who went looking for it twice would be daft).

Things that went wrong

The self-sealing exclusion. An app that learns only from refusals ends up confidently wrong and never finds out, because it stopped showing you the thing it was wrong about. In simulation the learner settled into a permanently narrow pool inside about 200 swipes. The fix is probes on excluded facets at growing intervals, which put the facet back if accepted.

It banned food I actually like. My own profile had five proteins blocked, three of them things I had saved repeatedly. Pork had four saves against one refusal and got blocked anyway. The one-tap "don't eat this" built its conclusion with hardcoded certainty and never looked at the evidence already sitting there. Now it stays quiet when the two disagree and lets the counted learner decide.

A block I could not put back. Lifting an exclusion gagged the learner about that facet for good, so I could unblock chicken, change my mind, and never block it again. Three separate permanent locks, all switched on by one tap.

The same three sides every time. Every plate offered Bakoula, Mangu and Krautfleckerl whatever the main was. The list sorted by gaps filled and then by score and took the top three, but gaps filled is a number from 0 to 3, so hundreds of the 906 offerable sides tie on it, and the tie was broken by a score that barely moves for side dishes. The card screen had sampled from a softmax since the first build for exactly this reason. I had never done the same for the plate.

A second dinner is not a side. 244 dishes are tagged both main and side, correctly, because mac and cheese really is eaten both ways. So is chili con carne, and offering that next to grilled chicken is offering two dinners. What separates them turned out not to be the slot but what the dish is built around. Every sensible one is dairy, egg, legume or nothing. Every absurd one is built on meat or fish.

What's next

Ingredient level exclusions. "No mushrooms" needs a per-dish ingredient list and the current data cannot support it, because the blurb is capped at sixty characters and only ever names the headline ingredient. A half working ingredient filter is worse than none, you would stop checking and then it serves you a stroganoff. That needs a new data pass, not a text scan.

After that, a shopping list handoff, and letting the plate learn which combinations a household actually puts together.


Built with Kotlin, Jetpack Compose, Room, SQLite, Python and RevenueCat. Android 8.0 and up.

Built With

Share this project:

Updates

Submission history