<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Leandro Favarin</title><link>http://leandrofavarin.com/</link><description>Recent content in Home on Leandro Favarin</description><generator>Hugo</generator><language>en</language><lastBuildDate>Mon, 03 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="http://leandrofavarin.com/feed.xml" rel="self" type="application/rss+xml"/><item><title>Scheduling Scripts on macOS with launchd</title><link>http://leandrofavarin.com/scheduling-scripts-on-macos-with-launchd/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>http://leandrofavarin.com/scheduling-scripts-on-macos-with-launchd/</guid><description>&lt;p>While I&amp;rsquo;ve used Google Photos extensively as a central storage for my personal photos, Apple Photos has recently taken over this responsibility. This article explains how I replaced an always-on service that archived my Google Photos with a minimally viable macOS launchd agent for Apple Photos.&lt;/p>
&lt;p>It&amp;rsquo;s great that we can rely on Google and Apple to keep our photos as a one copy, one storage medium, and one off-site location, but that&amp;rsquo;s not enough to meet the &lt;a href="https://library.vassar.edu/specialcollections/recordsmanagement#:~:text=This%203%2D2%2D1%20backup,hard%20drive%2C%20or%20USB%20drive.">“3–2–1 rule”&lt;/a>. We also need local backups that we’re continuously synchronizing to act as a second copy and a second storage medium.&lt;/p>
&lt;p>Apple Photos exists within the confines of iCloud and, as a consequence, if you want to automate backups in the same way that you can with GPhotos, you need to provide projects like &lt;a href="https://github.com/mandarons/icloud-docker">this&lt;/a> or &lt;a href="https://github.com/steilerDev/icloud-photos-sync">this&lt;/a> your iCloud password. With Jake Wharton&amp;rsquo;s &lt;code>docker-gphotos-sync&lt;/code> &lt;a href="https://github.com/JakeWharton/docker-gphotos-sync">project&lt;/a>, you are free to use your real password, or (preferably) create an app-specific password, sandboxing potential incidents from spreading further. There does not seem to exist a similar tool for Apple Photos. Although I would prefer a more versatile and reliable tool, hacking something fast was more important.&lt;/p>
&lt;p>Since I have the entirety of my library local, I could define a job on my &lt;a href="https://en.wikipedia.org/wiki/Cron">crontab&lt;/a> to clone its contents to another medium using standard CLI tools. However, cron is no longer the recommended approach for running headless tasks on a Mac. Per Apple&amp;rsquo;s &lt;a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html">documentation&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>If you are running per-user background processes for OS X, launchd is also the preferred way to start these processes. These per-user processes are referred to as user agents. A user agent is essentially identical to a daemon, but is specific to a given logged-in user and executes only while that user is logged in.&lt;/p>&lt;/blockquote>
&lt;p>The scheduling method used in macOS is not only preferable, but also significantly better than traditional approaches such as cron jobs. For example, if a computer is asleep when a task is scheduled to run, macOS will automatically reschedule the job to execute once the system wakes up. In contrast, cron jobs are completely ignored and only run when the computer is awake.&lt;/p>
&lt;p>It&amp;rsquo;s not particularly complicated to set up a &lt;em>hello world&lt;/em> for a user&amp;rsquo;s launch agent (&lt;a href="https://wiki.freepascal.org/macOS_daemons_and_agents#Launch_Daemon_to_run_a_script_as_a_user_on_a_schedule">example&lt;/a>), so I started with a simple shell script that invoked &lt;a href="https://rsync.samba.org">rsync&lt;/a> to mirror my Photos Library directory to my NAS. But recent security improvements in macOS have tightened the system up to a point where much of the internet literature became outdated. An example is &lt;a href="https://apple.stackexchange.com/questions/338213/how-to-run-a-launchagent-that-runs-a-script-which-causes-failures-because-of-sys">not being able to access&lt;/a> user&amp;rsquo;s directories (such as &lt;code>~/Pictures&lt;/code> or &lt;code>~/Documents&lt;/code>) via terminal emulators unless they&amp;rsquo;ve been given the &lt;em>Full Disk Access&lt;/em> permission. The specific error I got was &lt;code>rsync: opendir &amp;quot;~/Pictures/Photo Library&amp;quot; failed: Permission denied (13)&lt;/code>.&lt;/p>
&lt;p>A workaround would be to disable &lt;a href="https://en.wikipedia.org/wiki/System_Integrity_Protection">SIP&lt;/a> or &lt;a href="https://support.apple.com/guide/mac-help/change-privacy-security-settings-on-mac-mchl211c911f/mac">allow bash/zsh FDA&lt;/a>, but a much safer approach would be to allow only rsync access these directories. Unfortunately, even this did not solve the issue, as confirmed by several developers in the Stack Overflow thread linked above. A suggested workaround was to create a dedicated application that runs the script and give it FDA. This approach worked for me on macOS Sequoia 15.3.&lt;/p>
&lt;p>In short, you want the following:&lt;/p>
&lt;ul>
&lt;li>An application (I used Automator.app to create one) that runs a shell script that executes rsync with the correct arguments.&lt;/li>
&lt;li>A .plist file inside &lt;code>~/Library/LaunchAgents/&lt;/code> that invokes this application &lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/li>
&lt;/ul>
&lt;p>In my case, the application contained &lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>/usr/bin/rsync -azv --delete-after &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/Users/leandro/Pictures/Photos Library.photoslibrary/&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> mynas:&lt;span style="color:#ae81ff">\&amp;#39;&lt;/span>&lt;span style="color:#e6db74">&amp;#39;Photos Library.photoslibrary/&amp;#39;&lt;/span>&lt;span style="color:#ae81ff">\&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With &lt;code>local.PhotoSync.plist&lt;/code> at &lt;code>~/Library/LaunchAgents/&lt;/code> being:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&amp;lt;!DOCTYPE plist PUBLIC &amp;#34;-//Apple//DTD PLIST 1.0//EN&amp;#34; &amp;#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;#34;&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;plist&lt;/span> &lt;span style="color:#a6e22e">version=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;1.0&amp;#34;&lt;/span>&lt;span style="color:#f92672">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;dict&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;key&amp;gt;&lt;/span>Label&lt;span style="color:#f92672">&amp;lt;/key&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;string&amp;gt;&lt;/span>local.PhotoSync&lt;span style="color:#f92672">&amp;lt;/string&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;key&amp;gt;&lt;/span>ProgramArguments&lt;span style="color:#f92672">&amp;lt;/key&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;array&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;string&amp;gt;&lt;/span>/usr/bin/open&lt;span style="color:#f92672">&amp;lt;/string&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;string&amp;gt;&lt;/span>/Users/leandro/Applications/PhotoSync.app&lt;span style="color:#f92672">&amp;lt;/string&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/array&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;key&amp;gt;&lt;/span>StartCalendarInterval&lt;span style="color:#f92672">&amp;lt;/key&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;dict&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;key&amp;gt;&lt;/span>Hour&lt;span style="color:#f92672">&amp;lt;/key&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;integer&amp;gt;&lt;/span>0&lt;span style="color:#f92672">&amp;lt;/integer&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;key&amp;gt;&lt;/span>Minute&lt;span style="color:#f92672">&amp;lt;/key&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;integer&amp;gt;&lt;/span>0&lt;span style="color:#f92672">&amp;lt;/integer&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/dict&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/dict&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/plist&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>to run at every midnight, as specified with &lt;code>StartCalendarInterval&lt;/code>. With them in place, the commands to register&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> &amp;amp; unregister the agent on &lt;code>launchd&lt;/code> are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>launchctl load -k &amp;lt;your-plist&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>launchctl unload &amp;lt;your-plist&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The man pages of &lt;a href="https://ss64.com/mac/launchctl.html">launchctl&lt;/a> and &lt;a href="https://manpagez.com/man/5/launchd.plist/">launchd.plist&lt;/a> are fairly easy to understand and cover a lot of details not mentioned in this article.&lt;/p>
&lt;p>By setting an automated, in-background process that constantly replicates my photos to a secure offsite storage, I&amp;rsquo;ve removed Apple as a single point of failure in the event of unexpected account closure.&lt;/p>
&lt;p>I believe that the security and preservation of our digital memories should be a top priority and were we must actively more actions than to rely on a free service to magically exist and serve us.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>The command &lt;code>plutil -lint &amp;lt;plist-file&amp;gt;&lt;/code> can be used to validate the syntax.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The pair of single quotes above is necessary to preserve the space of the destination path, which is interpreted on the target machine.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>the &lt;code>-k&lt;/code> argument kills any running instance before restarting the service.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Drive your UI with SQLDelight’s views</title><link>http://leandrofavarin.com/drive-your-ui-with-sqldelights-views/</link><pubDate>Sun, 22 Sep 2019 00:00:00 +0000</pubDate><guid>http://leandrofavarin.com/drive-your-ui-with-sqldelights-views/</guid><description>&lt;p>Users demand more from apps every year and 2019 feels as though this pressure has never been so high. Among many ways we can improve the user experience as developers, driving the interface from a local database is an easy way to achieve robustness and flexibility. This is especially the case with database tools that can broadcast changes in the data set to interested parties.&lt;/p>
&lt;p>Suppose you’re writing a database-driven application using &lt;a href="https://github.com/cashapp/sqldelight">SQLDelight&lt;/a>, and you need to present the same type of data in slightly different ways. It could be that they differ on how they’re ordered, or just a subset of them.&lt;/p>
&lt;p>SQLDelight automatically generates model objects for every query that you write. In many cases, the types it generates are either from a single column (&lt;code>SELECT id FROM Foo&lt;/code>) or from the star projection (&lt;code>SELECT * FROM Foo&lt;/code>). The former being simple Kotlin String/Long/Int types, and the latter the type SQLDelight generates from your table definition. What happens, then, if you select a subset of the properties declared on your table?&lt;/p>
&lt;p>In this case SQLDelight will create a new type for every query, with its name being the named query:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>bandsOrderedByName:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> id, name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> band
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> name &lt;span style="color:#66d9ef">DESC&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bandsOrderedByAge:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> id, name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> band
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> age;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A simplified code of what SQLDelight generates is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">data&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BandsOrderedByName&lt;/span>(id: String, name: String)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">data&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BandsOrderedByAge&lt;/span>(id: String, name: String)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you wanted to transform them &lt;em>later on&lt;/em>, you would have to write two functions of types &lt;code>(BandsOrderedByName) -&amp;gt; X&lt;/code> and &lt;code>(BandsOrderedByAge) -&amp;gt; X&lt;/code>, even though the underlying composition is the same: two &lt;code>String&lt;/code>s.&lt;/p>
&lt;p>While this is a simple exercise, in real-life examples projections tend to be more complex, usually with &lt;a href="https://www.sqlite.org/syntax/join-clause.html">join clauses&lt;/a> and many properties selected. For example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> band.id, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> band.name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> album.&lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> band
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> album &lt;span style="color:#66d9ef">ON&lt;/span> band.id &lt;span style="color:#f92672">=&lt;/span> album.band_id; 
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A SQL &lt;a href="https://sqlite.org/lang_createview.html">View&lt;/a> solves this problem elegantly, with our queries now being:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">VIEW&lt;/span> bandWithAlbum &lt;span style="color:#66d9ef">AS&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> band.id, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> band.name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> album.&lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> band
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> album &lt;span style="color:#66d9ef">ON&lt;/span> band.id &lt;span style="color:#f92672">=&lt;/span> album.band_id; 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bandsOrderedByName:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> bandWithAlbum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> name &lt;span style="color:#66d9ef">DESC&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bandsOrderedByAge:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> bandWithAlbum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> age;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SQLDelight will generate &lt;code>BandWithAlbum&lt;/code> type that can be queried, ordered, and filtered differently, per query. Much of the SELECT boilerplate from the queries is eliminated while still allowing for specific constraints, additional joins for scoping, as well as selecting as a list or as a single item. Moreover, it creates a solid building block for pagination, with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">count&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#66d9ef">count&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> bandWithAlbum;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>paged:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> bandWithAlbum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LIMIT&lt;/span> &lt;span style="color:#f92672">?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">OFFSET&lt;/span> &lt;span style="color:#f92672">?&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Minimal effort is then needed to present items to the user by using only one &lt;a href="https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter">RecyclerView.Adapter&lt;/a>.&lt;/p>
&lt;p>A combination of any out-of-the-box extension such as for &lt;a href="https://github.com/cashapp/sqldelight/tree/master/extensions/coroutines-extensions">Kotlin Coroutines&lt;/a> or &lt;a href="https://github.com/cashapp/sqldelight/tree/master/extensions/rxjava2-extensions">RxJava&lt;/a> plus &lt;a href="https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil">DiffUtil&lt;/a> allows the user to easily visualize the data the way they want, because since the model being generated is a &lt;a href="https://kotlinlang.org/docs/reference/data-classes.html">value class&lt;/a>, the diff callbacks can be trivially written. One example would be:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">object&lt;/span> &lt;span style="color:#a6e22e">BandItemCallback&lt;/span> : ItemCallback&amp;lt;BandWithAlbum&amp;gt;() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">override&lt;/span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">areItemsTheSame&lt;/span>(oldItem: BandWithAlbum, newItem: BandWithAlbum): Boolean {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> oldItem.id &lt;span style="color:#f92672">==&lt;/span> newItem.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">override&lt;/span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">areContentsTheSame&lt;/span>(oldItem: BandWithAlbum, newItem: BandWithAlbum): Boolean {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> oldItem &lt;span style="color:#f92672">==&lt;/span> newItem
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Going back to the queries with different sorting, one could use an enum class to model the sort options and use Kotlin’s &lt;a href="https://kotlinlang.org/docs/reference/control-flow.html#when-expression">&lt;code>when&lt;/code> expression&lt;/a> to write:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">enum&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Sort&lt;/span> { NAME, AGE }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">bandsSorted&lt;/span>(&lt;span style="color:#66d9ef">by&lt;/span>: Sort): Flow&amp;lt;List&amp;lt;BandWithAlbum&amp;gt;&amp;gt; = &lt;span style="color:#66d9ef">when&lt;/span> (&lt;span style="color:#66d9ef">by&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NAME &lt;span style="color:#f92672">-&amp;gt;&lt;/span> db.bandsOrderedByName()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AGE &lt;span style="color:#f92672">-&amp;gt;&lt;/span> db.bandsOrderedByAge()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}.asFlow().mapToList()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and delegate to SQL algorithms that would be &lt;a href="https://speakerdeck.com/jakewharton/the-resurgence-of-sql-droidcon-nyc-2017?slide=123">less efficient&lt;/a> to run if they were executed programmatically.&lt;/p>
&lt;p>Building views for the types you want and then querying those views for the data you need is an easy way to attain responsiveness and flexibility with very little code needed. It’s clear that user demands will only get more intense as technology develops. It’s therefore crucial to stay on top of these simple ways to improve the experience without breaking our backs (or keyboards).&lt;/p>
&lt;hr>
&lt;p>&lt;em>Many thanks to &lt;a href="https://jakewharton.com">Jake Wharton&lt;/a> and &lt;a href="https://www.alecstrong.com">Alec Strong&lt;/a> for proofreading this article!&lt;/em>&lt;/p></description></item><item><title>Don't fear SQL - A better way to store and handle data with SQLBrite &amp; SQLDelight</title><link>http://leandrofavarin.com/dont-fear-sql-a-better-way-to-store-and-handle-data-with-sqlbrite-sqldelight/</link><pubDate>Fri, 14 Jul 2017 00:00:00 +0000</pubDate><guid>http://leandrofavarin.com/dont-fear-sql-a-better-way-to-store-and-handle-data-with-sqlbrite-sqldelight/</guid><description>&lt;p>For many people, SQL can be intimidating when writing mobile apps. To avoid its complexity, various libraries started to implement techniques such as Object-Relational Mapping. They ease basic operations on complex objects, but come with a set of downsides such as decreased performance and the learning curve of a new library.&lt;/p>
&lt;p>Square’s libraries SQLBrite and SQLDelight improve data manipulation by embracing all the powerful capabilities of SQLite while removing common frictions like runtime crashes, boilerplate code, and type-unsafe APIs.&lt;/p>
&lt;p>In this talk I’ll present the reactive mindset behind SQLBrite and the code-generation capabilities of SQLDelight. When combined, these two libraries will help you architect and code safer and faster with queries autocompletion, code reuse, and much more.&lt;/p></description></item><item><title>Exponential-Backoff RxJava operator with Jitter</title><link>http://leandrofavarin.com/exponential-backoff-rxjava-operator-with-jitter/</link><pubDate>Sat, 11 Mar 2017 00:00:00 +0000</pubDate><guid>http://leandrofavarin.com/exponential-backoff-rxjava-operator-with-jitter/</guid><description>&lt;p>Stripe recently published a technical &lt;a href="https://stripe.com/blog/idempotency">article&lt;/a> about how they handle errors between the clients and the server. The whole post is worth a read, and one of the topics mentioned was “Being a good distributed citizen”, in which mobile clients play fair when receiving failures from network operations. From their article (emphasis mine):&lt;/p>
&lt;blockquote>
&lt;p>It’s usually recommended that clients follow something akin to an &lt;a href="https://en.wikipedia.org/wiki/Exponential_backoff">exponential backoff&lt;/a> algorithm as they see errors. The client blocks for a brief initial wait time on the first failure, but as the operation continues to fail, it waits proportionally to 2^n, where n is the number of failures that have occurred. &lt;strong>By backing off exponentially, we can ensure that clients aren’t hammering on a downed server and contributing to the problem&lt;/strong>.&lt;/p>
&lt;p>Exponential backoff has a long and interesting &lt;a href="http://www.cs.utexas.edu/users/lam/NRL/backoff.html">history&lt;/a> in computer networking.&lt;/p>
&lt;p>Furthermore, &lt;strong>it’s also a good idea to mix in an element of randomness&lt;/strong>. If a problem with a server causes a large number of clients to fail at close to the same time, then even with back off, their retry schedules could be aligned closely enough that the retries will hammer the troubled server. This is known as &lt;a href="https://en.wikipedia.org/wiki/Thundering_herd_problem">the thundering herd problem&lt;/a>.&lt;/p>
&lt;p>We can address thundering herd by &lt;strong>adding some amount of random “jitter” to each client’s wait time&lt;/strong>. This will space out requests across all clients, and give the server some breathing room to recover.&lt;/p>&lt;/blockquote>
&lt;p>Network failures are everywhere and can happen at any time, and if you have several clients failing at the same time, it is a BIG deal.&lt;/p>
&lt;p>Since I couldn’t find any flexible yet powerful solution for this problem written in Kotlin I decided to write my own using &lt;a href="https://github.com/ReactiveX/RxJava">RxJava 2&lt;/a>. The operator is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * Exponential backoff that respects the equation: delay * retries ^ 2 * jitter
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">ExpBackoff&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> jitter: Jitter,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> delay: Long,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> unit: TimeUnit,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> retries: Int = &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) : Function&amp;lt;Observable&amp;lt;&lt;span style="color:#66d9ef">out&lt;/span> Throwable&amp;gt;, Observable&amp;lt;Long&amp;gt;&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Throws&lt;/span>(Exception&lt;span style="color:#f92672">::&lt;/span>&lt;span style="color:#66d9ef">class&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">override&lt;/span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">apply&lt;/span>(observable: Observable&amp;lt;&lt;span style="color:#66d9ef">out&lt;/span> Throwable&amp;gt;): Observable&amp;lt;Long&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> observable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .zipWith(&lt;span style="color:#a6e22e">Observable&lt;/span>.range(&lt;span style="color:#ae81ff">1&lt;/span>, retries), BiFunction&amp;lt;Throwable, Int, Int&amp;gt; { _, retryCount &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> retryCount
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .flatMap { attemptNumber &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Observable&lt;/span>.timer(getNewInterval(attemptNumber), unit) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">getNewInterval&lt;/span>(retryCount: Int): Long {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> newInterval = (delay * &lt;span style="color:#a6e22e">Math&lt;/span>.pow(retryCount.toDouble(), &lt;span style="color:#ae81ff">2.0&lt;/span>) * jitter.&lt;span style="color:#66d9ef">get&lt;/span>()).toLong()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (newInterval &amp;lt; &lt;span style="color:#ae81ff">0&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newInterval = &lt;span style="color:#a6e22e">Long&lt;/span>.MAX_VALUE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> newInterval
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With &lt;code>Jitter&lt;/code> being:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">interface&lt;/span> &lt;span style="color:#a6e22e">Jitter&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">get&lt;/span>(): Double
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">companion&lt;/span> &lt;span style="color:#66d9ef">object&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">val&lt;/span> NO_OP = { &lt;span style="color:#ae81ff">1&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A default implementation that could deviate up to 15% could be written as:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">DefaultJitter&lt;/span> : Jitter {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> random = Random()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** Returns a random value inside [0.85, 1.15] every time it&amp;#39;s called */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">override&lt;/span> &lt;span style="color:#66d9ef">fun&lt;/span> &lt;span style="color:#a6e22e">get&lt;/span>(): Double = &lt;span style="color:#ae81ff">0.85&lt;/span> + random.nextDouble() % &lt;span style="color:#ae81ff">0.3f&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The implementation here is not 1:1 to what Stripe did, but it could be changed easily to adapt to your needs.&lt;/p>
&lt;p>Its usage is then very simple. Just apply it before subscribing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span style="display:flex;">&lt;span>observable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">..&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .retryWhen(ExpBackoff(DefaultJitter(), delay = &lt;span style="color:#ae81ff">1&lt;/span>, unit = SECONDS, retries = &lt;span style="color:#ae81ff">3&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .subscribe(&lt;span style="color:#75715e">/* */&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item></channel></rss>