<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Blake Riding</title>
    <description>The latest articles on DEV Community by Blake Riding (@riding).</description>
    <link>https://dev.to/riding</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F802798%2F141ef95f-bbbe-44c1-97a3-53acf2f23946.jpeg</url>
      <title>DEV Community: Blake Riding</title>
      <link>https://dev.to/riding</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/riding"/>
    <language>en</language>
    <item>
      <title>I built an Android app for remembering the little things about people you love (no AI, no internet permission)</title>
      <dc:creator>Blake Riding</dc:creator>
      <pubDate>Mon, 26 Oct 2026 22:01:48 +0000</pubDate>
      <link>https://dev.to/riding/i-built-an-android-app-for-remembering-the-little-things-about-people-you-love-no-ai-no-internet-8d1</link>
      <guid>https://dev.to/riding/i-built-an-android-app-for-remembering-the-little-things-about-people-you-love-no-ai-no-internet-8d1</guid>
      <description>&lt;p&gt;My brain drops things. Not the big stuff, but the small stuff that makes someone feel known: her coffee order, that she stopped to look at a blue scarf, that she doesn't want to always be the one who plans everything.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Little Things&lt;/strong&gt;, an Android app for people with ADHD (or just busy brains) to capture those details the moment they come up and get them resurfaced later, when they're useful.&lt;/p&gt;

&lt;p&gt;This post is about the few design decisions that mattered, and the code behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint: capture has to take about five seconds
&lt;/h2&gt;

&lt;p&gt;If saving a note takes more than a tap or two, I won't do it in the moment, and then the note never exists. So the home screen has one big button, the keyboard opens immediately, and there's a "Save + another" option for brain dumps.&lt;/p&gt;

&lt;p&gt;The part that makes it feel instant is that you never pick a category. You just type, and the app guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Smart" categorization without AI
&lt;/h2&gt;

&lt;p&gt;Everyone's first assumption is that this uses a model. It doesn't. It looks for key words and phrases in what you type, and nothing leaves the phone.&lt;/p&gt;

&lt;p&gt;The whole thing is one function on the &lt;code&gt;Category&lt;/code&gt; enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;" ${text.lowercase().replace('’', '\'')} "&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;vararg&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doesn't like"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dislike"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"allergic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"avoid"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;DISLIKES&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doesn't want to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"needs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tired of"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"overwhelmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mental load"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NEEDS&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" xs "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" xl "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"inseam"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;SIZES&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"present"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"wishlist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"for christmas"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;GIFT_IDEAS&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"favorite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"favourite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" fav "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;FAVORITES&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"want to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"someday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bucket list"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;WANTS&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"coffee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" tea "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"latte"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pizza"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"vegan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"snack"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;FOOD&lt;/span&gt;
        &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loves"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"likes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enjoys"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"obsessed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"adores"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LOVES&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Trimmed here for the post; the real lists are longer.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order is the whole trick.&lt;/strong&gt; "Doesn't like cilantro" contains "like", so if &lt;code&gt;LOVES&lt;/code&gt; were checked first, you'd file a dislike under loves, which is the worst possible bug in an app about remembering what people care about. Negations go first: "doesn't like" beats "likes", and "doesn't want to" beats "want to". There's a unit test pinned to exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;dislikeBeatsLike&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DISLIKES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doesn't like cilantro"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When nothing matches, &lt;code&gt;guess&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; and the user picks manually. A wrong guess is annoying; a missing guess is just one extra tap. I'd rather be conservative.&lt;/p&gt;

&lt;p&gt;Why not a model? Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; These notes are about real people in someone's life. The app declares one permission, &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;. There is no &lt;code&gt;INTERNET&lt;/code&gt; permission at all, so it physically can't phone home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency and reliability.&lt;/strong&gt; A &lt;code&gt;when&lt;/code&gt; block returns instantly, offline, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability.&lt;/strong&gt; When it guesses wrong, I add a phrase and a test. No prompt tuning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The daily pick: shuffled, but stable
&lt;/h2&gt;

&lt;p&gt;The home screen and the home-screen widget both show "a little thing for today." They have to agree, and they have to change every day, without storing anything extra.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;TodaysPick&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;epochDay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;sortedBy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;shuffled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epochDay&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort by id first so the input order is deterministic, then shuffle with a &lt;code&gt;Random&lt;/code&gt; seeded by the epoch day. Same day, same order, on every surface. New day, new order. No database column, no sync between the app and the widget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nudges that rotate instead of repeat
&lt;/h2&gt;

&lt;p&gt;A daily &lt;code&gt;WorkManager&lt;/code&gt; job handles date reminders and the occasional nudge. For nudges I didn't want pure random, because random will happily show you the same note three times in a week while others never come up.&lt;/p&gt;

&lt;p&gt;So each note records when it was last surfaced, and the query returns the eight least-recently-surfaced notes. The worker picks randomly from those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;note&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leastRecentlySurfaced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;randomOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything eventually comes up, but it doesn't feel like a queue marching in order.&lt;/p&gt;

&lt;p&gt;Some categories also get a follow-up question that nudges toward action instead of just recall. A &lt;code&gt;NEEDS&lt;/code&gt; note adds "Anything small you can do for Sam today?" and a &lt;code&gt;GIFT_IDEAS&lt;/code&gt; note adds "Maybe this is the week?"&lt;/p&gt;

&lt;p&gt;One gotcha worth sharing: &lt;code&gt;WorkManager&lt;/code&gt; doesn't fire at the exact same minute daily. My nudge check compares elapsed time against the frequency minus three hours of slack, otherwise a "daily" nudge sometimes skips a day because the job ran a few minutes early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Birthday reminders that carry a number
&lt;/h2&gt;

&lt;p&gt;For important dates the notification does one more thing: it counts your saved gift ideas.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tomorrow: Sam's birthday&lt;/strong&gt;&lt;br&gt;
You have 3 saved gift ideas. Tap to see them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or, if you have none: "No gift ideas saved yet. Tap to see what they love." The reminder isn't just a date, it's a prompt to use what you've already captured. A small query (&lt;code&gt;countActiveGiftIdeas&lt;/code&gt;) makes it much more useful than a bare calendar alert.&lt;/p&gt;

&lt;p&gt;The date math had a few traps, so it's covered by tests: Feb 29 falling back to Feb 28 in non-leap years, past dates rolling to next year, and optional years so "anniversary, 5 years" works but a birthday with no year doesn't invent a count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups without a server
&lt;/h2&gt;

&lt;p&gt;No cloud means no sync, so backup is a single &lt;code&gt;.zip&lt;/code&gt; the user keeps wherever they like: a &lt;code&gt;data.json&lt;/code&gt; plus a &lt;code&gt;photos/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Two details I'm glad I did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parse everything before touching the database.&lt;/strong&gt; Import decodes the whole file into objects first, then wipes and inserts inside one Room transaction. A corrupt file can't leave you with an empty app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard zip entry names.&lt;/strong&gt; Restoring from an archive means writing files from untrusted names, so anything with a path separator or &lt;code&gt;..&lt;/code&gt; is rejected. Classic zip-slip, easy to forget in a hobby app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Kotlin, Jetpack Compose (Material 3), Room, WorkManager, Navigation Compose, and Glance for the widget. Min Android 8.0 (API 26).&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Little Things is an Android app: &lt;br&gt;
&lt;a href="https://play.google.com/store/apps/details?id=com.blakeriding.littlethings&amp;amp;referrer=utm_source%3Dwebsite%26utm_medium%3Dreferral%26utm_campaign%3Dsite_hero" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.blakeriding.littlethings&amp;amp;referrer=utm_source%3Dwebsite%26utm_medium%3Dreferral%26utm_campaign%3Dsite_hero&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear which phrases it guesses wrong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The repo is private for now, but I'm happy to answer questions about any of the code above in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
