<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://vishnurajeevan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vishnurajeevan.com/" rel="alternate" type="text/html" /><updated>2025-11-17T15:56:36+00:00</updated><id>https://vishnurajeevan.com/feed.xml</id><title type="html">Blog</title><subtitle>Just stuff</subtitle><author><name>Vishnu Rajeevan</name></author><entry><title type="html">Self Hosting Your Audiobooks</title><link href="https://vishnurajeevan.com/selfhosting-audiobooks/" rel="alternate" type="text/html" title="Self Hosting Your Audiobooks" /><published>2024-12-30T00:00:00+00:00</published><updated>2024-12-30T00:00:00+00:00</updated><id>https://vishnurajeevan.com/selfhosting-audiobooks</id><content type="html" xml:base="https://vishnurajeevan.com/selfhosting-audiobooks/"><![CDATA[<p>4 years ago I made the switch from Audible to Libro.fm. Libro.fm serves DRM free audiobooks, shares revenue with a local bookshop you select and works on a similar monthly subscription/credit system to Audible.</p>

<p>However, one issue arises: how I listen to the books? The official mobile app is good, but part of owning DRM free files is storing/archiving them locally. To start I had setup a Plex audiobook library but ran into a lot of the edges of running a “non-standard” library with Plex. Things like metadata, cover images and official app support are either hacky or manually managed. I was willing to do deal with it because the third party <a href="https://prologue.audio/">app</a> I was using was so good. Eventually, the manual work with Plex became overwhelming and I felt the need to switch to something more purpose built.</p>

<p><a href="https://www.audiobookshelf.org/">Audiobookshelf</a> ended up being the choice I went with. Solid metadata and management features as well as OpenID Connect authentication. Plus its an easy service to run:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">audiobookshelf</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/advplyr/audiobookshelf:latest</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/media/audiobooks:/audiobooks</span>
      <span class="pi">-</span> <span class="s">/mnt/user/media/books:/books</span>
      <span class="pi">-</span> <span class="s">/mnt/user/media/comics:/comics</span>
      <span class="pi">-</span> <span class="s">/mnt/runtime/appdata/audiobookshelf/config:/config</span>
      <span class="pi">-</span> <span class="s">/mnt/runtime/appdata/audiobookshelf/metadata:/metadata</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="na">traefik.enable</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">traefik.http.routers.abs.entrypoints</span><span class="pi">:</span> <span class="s">websecure</span>
      <span class="na">traefik.http.routers.abs.rule</span><span class="pi">:</span> <span class="s">Host(`&lt;&gt;`)</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">user=99:100</span>
<span class="na">networks</span><span class="pi">:</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>I’ve been using a third party <a href="https://github.com/LeoKlaus/plappa">app</a> because the official iOS app has a full testflight. This setup has worked <em>really well</em> but there was still one bit of friction with the workflow:</p>

<ol>
  <li>Purchase book from libro.fm</li>
  <li>Download files via browser</li>
  <li>Unzip files</li>
  <li>Upload files to the server. Used to be via samba, but with audiobookshelf I was able to upload via the webapp.</li>
</ol>

<p>I have almost 200 audiobooks, and this year I’d added 15+ so this got annoying very quickly.</p>

<p>I saw <a href="https://github.com/advplyr/audiobookshelf/issues/2112">this issue</a> and agreed that the enhancement is out of scope for ABS, but <a href="https://github.com/advplyr/audiobookshelf/issues/2112#issuecomment-1866724546">this comment</a> made me realize I could just write my own service to handle the responsibility. So, <a href="https://github.com/burntcookie90/librofm-downloader">I did</a>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">librofm-downloader</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/burntcookie90/librofm-downloader:latest</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/runtime/appdata/librofm-downloader:/data</span>
      <span class="pi">-</span> <span class="s">/mnt/user/media/audiobooks:/media</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="c1"># optional if you want to use the /update webhook</span>
      <span class="pi">-</span> <span class="s">8080:8080</span> 
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">LIBRO_FM_USERNAME=&lt;&gt;</span>
      <span class="pi">-</span> <span class="s">LIBRO_FM_PASSWORD=&lt;&gt;</span>
      <span class="pi">-</span> <span class="s">RENAME_CHAPTERS=true</span>
      <span class="pi">-</span> <span class="s">WRITE_TITLE_TAG=true</span> <span class="c1">#this one requires RENAME_CHAPTERS to be true as well</span>
</code></pre></div></div>

<p>Give it a shot!</p>

<style>
  :root {
    color-scheme: dark;
  }
  bsky-comments {
    --background-color: none;
    --text-color: rgba(255,255,255,0.8);
    --link-color: #0077cc;
    --link-hover-color: #005fa3;
    --comment-meta-color: rgba(255,255,255,0.5);
    --error-color: #ff4d4d;
    --reply-border-color: rgba(255,255,255,0.1);
    --button-background-color: rgba(255,255,255,0.1);
    --button-hover-background-color: rgba(255,255,255,0.2);
    --author-avatar-border-radius: 50%;
  }
</style>

<script type="module" src="https://esm.sh/gh/loueed/bsky@v1.0.0/comments"></script>

<bsky-comments post="at://did:plc:utqsoejrgbeaczfmiepczpax/app.bsky.feed.post/3lekaoklx322p"></bsky-comments>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[4 years ago I made the switch from Audible to Libro.fm. Libro.fm serves DRM free audiobooks, shares revenue with a local bookshop you select and works on a similar monthly subscription/credit system to Audible.]]></summary></entry><entry><title type="html">APIs In The Real World</title><link href="https://vishnurajeevan.com/real-world-apis/" rel="alternate" type="text/html" title="APIs In The Real World" /><published>2024-12-21T00:00:00+00:00</published><updated>2024-12-21T00:00:00+00:00</updated><id>https://vishnurajeevan.com/real-world-apis</id><content type="html" xml:base="https://vishnurajeevan.com/real-world-apis/"><![CDATA[<p>Anytime we host guests, its almost guaranteed we’ll get the following question: “Where’s y’all’s recycling?”. We have a dual bin combo recycling/trash can. The recycling side has a blue bag and the trash side is white. For my wife and I, this is enough to maintain our habits without confusion. However, for guests, folks have opened up the bin and asked to confirm “left side is recycling right?”.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">TrasherAndRecycler</span> <span class="p">{</span>
  <span class="k">fun</span> <span class="nf">trash</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>

  <span class="k">fun</span> <span class="nf">recycle</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After a few times of that, I decided to print out labels that you can see when you open the lid. <em>Even then</em> people open the lid with their foot and look over at me “which side is recycling?”.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">TrasherAndRecycler</span> <span class="p">{</span>

  <span class="cm">/**
  * Use this for garbage
  */</span>
  <span class="k">fun</span> <span class="nf">trash</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>

  <span class="cm">/**
  * Use this for recycling
  */</span>
  <span class="k">fun</span> <span class="nf">recycle</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I left it at that for years, resigning myself to live in the world of responding “left side of the bin, yep!”.</p>

<p>Last night, we were prepping to host a party and I decided to make the bin just trash, and put white bags in both and labeled both sides as “garbage”. I then put a blue bin next to it, as well as a second blue bag on the deck for recycling. This morning I realized that, not only did I not get asked even once where the recycling is, the blue bags and bins had <em>only</em> recycling in them!</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Trasher</span> <span class="p">{</span>
  <span class="cm">/**
  * Use this for garbage
  */</span>
  <span class="k">fun</span> <span class="nf">trash</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Recycler</span> <span class="p">{</span>
  <span class="cm">/**
  * Use this for recycling
  */</span>
  <span class="k">fun</span> <span class="nf">recycle</span><span class="p">(</span><span class="n">obj</span><span class="p">:</span> <span class="nc">Any</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The design of our trash bin obscures the “functionality” of the two containers within it. New guests step on the pedal to open the bin but then don’t notice the blue bag or the labeling and end up having to ask for confirmation.</p>

<p><img src="/assets/image/trash-bin.jpg" alt="" /></p>

<hr />

<p>Thanks <a href="https://bsky.app/profile/queencodemonkey.dev">Huyen</a> for the review!</p>

<style>
  :root {
    color-scheme: dark;
  }
  bsky-comments {
    --background-color: none;
    --text-color: rgba(255,255,255,0.8);
    --link-color: #0077cc;
    --link-hover-color: #005fa3;
    --comment-meta-color: rgba(255,255,255,0.5);
    --error-color: #ff4d4d;
    --reply-border-color: rgba(255,255,255,0.1);
    --button-background-color: rgba(255,255,255,0.1);
    --button-hover-background-color: rgba(255,255,255,0.2);
    --author-avatar-border-radius: 50%;
  }
</style>

<script type="module" src="https://esm.sh/gh/loueed/bsky@v1.0.0/comments"></script>

<bsky-comments post="at://did:plc:utqsoejrgbeaczfmiepczpax/app.bsky.feed.post/3ldw6cfpqac2h"></bsky-comments>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[Anytime we host guests, its almost guaranteed we’ll get the following question: “Where’s y’all’s recycling?”. We have a dual bin combo recycling/trash can. The recycling side has a blue bag and the trash side is white. For my wife and I, this is enough to maintain our habits without confusion. However, for guests, folks have opened up the bin and asked to confirm “left side is recycling right?”.]]></summary></entry><entry><title type="html">Self Hosting Bluesky PDS with Docker-Compose and Traefik</title><link href="https://vishnurajeevan.com/bluesky-pds/" rel="alternate" type="text/html" title="Self Hosting Bluesky PDS with Docker-Compose and Traefik" /><published>2024-11-19T00:00:00+00:00</published><updated>2024-11-19T00:00:00+00:00</updated><id>https://vishnurajeevan.com/bluesky-pds</id><content type="html" xml:base="https://vishnurajeevan.com/bluesky-pds/"><![CDATA[<p><strong>Update 11/26/2024</strong>: Added <code class="language-plaintext highlighter-rouge">PDS_DATA_DIRECTORY</code> environment variable to compose.yml. The <code class="language-plaintext highlighter-rouge">installer.sh</code> script in the pds repo unusually aliases <code class="language-plaintext highlighter-rouge">PDS_DATADIR</code> to this for actually binding the volume.</p>

<p>(This assumes you have a running traefik instance, and working domain/dns)</p>

<p><code class="language-plaintext highlighter-rouge">docker-compose.yml</code></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">pds</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">pds</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/bluesky-social/pds:0.4</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./bluesky/pds:/pds</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="na">traefik.enable</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">traefik.http.routers.bluesky-pds.rule</span><span class="pi">:</span> <span class="s">Host(`example.com`)</span>
      <span class="na">traefik.http.routers.bluesky-pds.entrypoints</span><span class="pi">:</span> <span class="s">websecure</span>
      <span class="na">traefik.http.routers.bluesky-pds.tls.certresolver</span><span class="pi">:</span> <span class="s">letsencrypttls</span>
      <span class="na">traefik.http.services.bluesky-pds.loadbalancer.server.port</span><span class="pi">:</span> <span class="m">3000</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">PDS_DATADIR=/pds</span>
      <span class="pi">-</span> <span class="s">PDS_DATA_DIRECTORY=/pds</span>
      <span class="pi">-</span> <span class="s">PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks</span>
      <span class="pi">-</span> <span class="s">PDS_HOSTNAME=</span> <span class="c1">#example.com</span>
      <span class="pi">-</span> <span class="s">PDS_JWT_SECRET=</span> <span class="c1">#openssl rand --hex 16</span>
      <span class="pi">-</span> <span class="s">PDS_ADMIN_PASSWORD=</span> <span class="c1">#openssl rand --hex 16</span>
      <span class="pi">-</span> <span class="s">PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=</span> <span class="c1">#openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32</span>
      <span class="pi">-</span> <span class="s">PDS_EMAIL_SMTP_URL=</span> <span class="c1">#smtp://username@gmail.com:password@smtp.gmail.com:587</span>
      <span class="pi">-</span> <span class="s">PDS_EMAIL_FROM_ADDRESS=</span> <span class="c1">#admin@domain.com</span>
      <span class="pi">-</span> <span class="s">PDS_MODERATION_EMAIL_SMTP_URL=</span> <span class="c1">#smtp://username@gmail.com:password@smtp.gmail.com:587</span>
      <span class="pi">-</span> <span class="s">PDS_MODERATION_EMAIL_ADDRESS=</span> <span class="c1">#admin@domain.com</span>
      <span class="pi">-</span> <span class="s">PDS_DID_PLC_URL=https://plc.directory</span>
      <span class="pi">-</span> <span class="s">PDS_BSKY_APP_VIEW_URL=https://api.bsky.app</span>
      <span class="pi">-</span> <span class="s">PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app</span>
      <span class="pi">-</span> <span class="s">PDS_REPORT_SERVICE_URL=https://mod.bsky.app</span>
      <span class="pi">-</span> <span class="s">PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac</span>
      <span class="pi">-</span> <span class="s">PDS_CRAWLERS=https://bsky.network</span>
      <span class="pi">-</span> <span class="s">LOG_ENABLED=true</span>
<span class="na">networks</span><span class="pi">:</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>next run the following to generate an invite:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--request</span> POST <span class="se">\</span>
  <span class="nt">--url</span> https://example.com/xrpc/com.atproto.server.createInviteCode <span class="se">\</span>
  <span class="nt">--header</span> <span class="s1">'Authorization: Basic &lt;token&gt;'</span> <span class="se">\</span>
  <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{
  "useCount": 1
}'</span>
</code></pre></div></div>

<p>Then you should be able to sign up on https://bsky.app with your $PDS_HOSTNAME as the hosting provider.</p>

<p>If you’re looking for mastodon check <a href="/self-hosting-mastodon-with-docker-compose-and-traefik/">here</a>.</p>

<p>If you’re looking for setting up traefik check <a href="/traefik/">here</a>.</p>

<p>Used <a href="https://therobbiedavis.com/selfhosting-bluesky-with-docker-and-swag/">https://therobbiedavis.com/selfhosting-bluesky-with-docker-and-swag/</a> as a resource for this.</p>

<style>
  :root {
    color-scheme: dark;
  }
  bsky-comments {
    --background-color: none;
    --text-color: rgba(255,255,255,0.8);
    --link-color: #0077cc;
    --link-hover-color: #005fa3;
    --comment-meta-color: rgba(255,255,255,0.5);
    --error-color: #ff4d4d;
    --reply-border-color: rgba(255,255,255,0.1);
    --button-background-color: rgba(255,255,255,0.1);
    --button-hover-background-color: rgba(255,255,255,0.2);
    --author-avatar-border-radius: 50%;
  }
</style>

<script type="module" src="https://esm.sh/gh/loueed/bsky@v1.0.0/comments"></script>

<bsky-comments post="at://did:plc:utqsoejrgbeaczfmiepczpax/app.bsky.feed.post/3lbulpxnenc2t"></bsky-comments>

<p>Comments via: <a href="https://github.com/LoueeD/bsky">https://github.com/LoueeD/bsky</a></p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[Update 11/26/2024: Added PDS_DATA_DIRECTORY environment variable to compose.yml. The installer.sh script in the pds repo unusually aliases PDS_DATADIR to this for actually binding the volume.]]></summary></entry><entry><title type="html">Controlling My R1T With Docker Home Assistant and Homekit</title><link href="https://vishnurajeevan.com/r1t-hass/" rel="alternate" type="text/html" title="Controlling My R1T With Docker Home Assistant and Homekit" /><published>2023-09-27T00:00:00+00:00</published><updated>2023-09-27T00:00:00+00:00</updated><id>https://vishnurajeevan.com/r1t-hass</id><content type="html" xml:base="https://vishnurajeevan.com/r1t-hass/"><![CDATA[<p>Recently, the <a href="https://github.com/bretterer/home-assistant-rivian/releases/tag/1.0.0-beta.6">Rivian Home Assistant (HASS)</a> added support for controlling the vehicle. 
Alongside the <a href="https://www.home-assistant.io/integrations/homekit/">HomeKit integration</a> this meant my truck was now available via iOS17 HomeKit widgets.
Unfortuantely (but also fortunately) it seems the method by which the Rivian integration worked was <a href="https://rivian.software/2023-34-00/">patched</a> which meant the integration needed to be updated for the commands to send to the truck. 
The folks were <a href="https://github.com/bretterer/home-assistant-rivian/releases/tag/1.0.0-beta.7">quick</a> and began iterating as beta users were reporting their experiences with the pairing process.</p>

<p>For me, this meant figuring out how to make the BT pairing work through docker with my truck 20ft away.</p>

<h2 id="home-assistant">Home Assistant</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">home-assistant</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">home-assistant</span>
    <span class="na">cap_add</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">NET_ADMIN</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">HDDTemp</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">TZ=America/New_York</span>
      <span class="pi">-</span> <span class="s">HOST_OS=Unraid</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">homeassistant/home-assistant</span>
    <span class="na">network_mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">host"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/configs/home-assistant:/config:rw</span>
      <span class="pi">-</span> <span class="s">/run/dbus:/run/dbus:ro</span>
      <span class="pi">-</span> <span class="s">/dev/bus/usb/001/009:/dev/bus/usb/001/009</span>
    <span class="na">working_dir</span><span class="pi">:</span> <span class="s">/config</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/dev/bus/usb/001/009:/dev/bus/usb/001/009</span>
</code></pre></div></div>

<p>The key information here is the <code class="language-plaintext highlighter-rouge">volume</code> bind for <code class="language-plaintext highlighter-rouge">/run/dbus</code> which allows the container to submit dbus messages for BT pairing (<a href="https://www.home-assistant.io/integrations/bluetooth/#additional-requirements-by-install-method">mentioned here</a>).</p>

<p>Additionally, I needed to map the USB device corresponding to my motherboard’s BT radio. 
This can be found by running <code class="language-plaintext highlighter-rouge">lsusb</code>, which for me returned this obscure entry: <code class="language-plaintext highlighter-rouge">Bus 001 Device 009: ID 8087:0033 Intel Corp.</code></p>

<h3 id="homekit">HomeKit</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">homekit</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">filter</span><span class="pi">:</span>
      <span class="na">include_domains</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">alarm_control_panel</span>
        <span class="pi">-</span> <span class="s">camera</span>
        <span class="pi">-</span> <span class="s">climate</span>
        <span class="pi">-</span> <span class="s">cover</span>
        <span class="pi">-</span> <span class="s">lock</span>
        <span class="pi">-</span> <span class="s">light</span>
        <span class="pi">-</span> <span class="s">media_player</span>
        <span class="pi">-</span> <span class="s">scene</span>
        <span class="pi">-</span> <span class="s">fan</span>
        <span class="pi">-</span> <span class="s">script</span>
        <span class="pi">-</span> <span class="s">switch</span>
        <span class="pi">-</span> <span class="s">input_boolean</span>
      <span class="na">exclude_entities</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">cover.cheddar_front_trunk</span>
        <span class="pi">-</span> <span class="s">cover.cheddar_windows</span>
</code></pre></div></div>

<p>This is the main part of my homekit config. I believe newer HASS instances can do homekit configuration via web, so take this as reference and not as a guide. 
This will create a HomeKit bridge entity that you pair with your Home app. 
Any entities in HASS that fit the domains will be proxied over.</p>

<h2 id="troubleshooting">Troubleshooting</h2>

<p>This process wasn’t as smooth as I’d hoped. My host machine is running Unraid, which doesn’t ship with <code class="language-plaintext highlighter-rouge">bluez</code>, this meant I needed to get <code class="language-plaintext highlighter-rouge">bluez</code> installed from <a href="https://slackware.pkgs.org/15.0/slackware-patches-x86_64/bluez-5.64-x86_64-1_slack15.0.txz.html">slackware</a>. From there, it seems my BT stack was either getting overwhelmed or running out of pairing slots for fun. 
I realized that toggling the power to the radio with <code class="language-plaintext highlighter-rouge">hciconfig</code> seemed to give me enough time to control other BT accessories with HASS before things started timing out. 
While sitting in the truck, I toggled BT power, then initiated the key pairing sequence. Eventually it worked</p>

<p><img src="/assets/image/IMG_3984.png" alt="" /></p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[Recently, the Rivian Home Assistant (HASS) added support for controlling the vehicle. Alongside the HomeKit integration this meant my truck was now available via iOS17 HomeKit widgets. Unfortuantely (but also fortunately) it seems the method by which the Rivian integration worked was patched which meant the integration needed to be updated for the commands to send to the truck. The folks were quick and began iterating as beta users were reporting their experiences with the pairing process.]]></summary></entry><entry><title type="html">Basic Reverse Proxy Setup With Traefik</title><link href="https://vishnurajeevan.com/traefik/" rel="alternate" type="text/html" title="Basic Reverse Proxy Setup With Traefik" /><published>2022-11-19T00:00:00+00:00</published><updated>2022-11-19T00:00:00+00:00</updated><id>https://vishnurajeevan.com/traefik</id><content type="html" xml:base="https://vishnurajeevan.com/traefik/"><![CDATA[<p>Make sure you point your domain to your home IP address. 
If you’re in need of dynamic DNS support, look into cloudflare or duckDNS. 
You can use something like <a href="https://hub.docker.com/r/oznu/cloudflare-ddns/">this</a> to keep it up to date.</p>

<p>My server doesn’t use the default 80/443, so I’ve used the docker <code class="language-plaintext highlighter-rouge">ports</code> block to bind to 81/444.</p>

<p>I use an external <code class="language-plaintext highlighter-rouge">proxy</code> docker network that containers in other stacks can attach to in order to hook to the reverse proxy.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">traefik</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="c1">#- "--log.level=DEBUG"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--api.insecure=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.exposedbydefault=false"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.network=proxy"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.web.address=:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.websecure.address=:443"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.myresolver.acme.tlschallenge=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.myresolver.acme.email=postmaster@example.com"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">log.filepath=/var/log/traefik.log</span>
      <span class="pi">-</span> <span class="s">TZ=America/New_York</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v2.9</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="na">traefik.enable</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">traefik.http.middlewares.redirect-to-https.redirectscheme.scheme</span><span class="pi">:</span> <span class="s">https</span>
      <span class="na">traefik.http.routers.http-catchall.entrypoints</span><span class="pi">:</span> <span class="s">web</span>
      <span class="na">traefik.http.routers.http-catchall.middlewares</span><span class="pi">:</span> <span class="s">redirect-to-https</span>
      <span class="na">traefik.http.routers.http-catchall.rule</span><span class="pi">:</span> <span class="s">HostRegexp(`{host:.+}`)</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">444:443/tcp</span>
      <span class="pi">-</span> <span class="s">81:80/tcp</span>
      <span class="pi">-</span> <span class="s">8081:8080/tcp</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock:ro</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/traefik/letsencrypt:/letsencrypt:rw</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/traefik/config/traefik.toml:/etc/traefik/traefik.toml:rw</span>
<span class="na">networks</span><span class="pi">:</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[Make sure you point your domain to your home IP address. If you’re in need of dynamic DNS support, look into cloudflare or duckDNS. You can use something like this to keep it up to date.]]></summary></entry><entry><title type="html">Self Hosting Mastodon with Docker-Compose and Traefik</title><link href="https://vishnurajeevan.com/self-hosting-mastodon-with-docker-compose-and-traefik/" rel="alternate" type="text/html" title="Self Hosting Mastodon with Docker-Compose and Traefik" /><published>2022-11-03T00:00:00+00:00</published><updated>2022-11-03T00:00:00+00:00</updated><id>https://vishnurajeevan.com/self-hosting-mastodon-with-docker-compose-and-traefik</id><content type="html" xml:base="https://vishnurajeevan.com/self-hosting-mastodon-with-docker-compose-and-traefik/"><![CDATA[<p>(This assumes you have a running traefik instance)</p>

<p>Use <code class="language-plaintext highlighter-rouge">.env.production</code> from <a href="https://github.com/mastodon/mastodon/blob/main/.env.production.sample">github</a>.</p>

<p>Things to edit:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">LOCAL_DOMAIN</span><span class="o">=</span>example.com
<span class="nv">WEB_DOMAIN</span><span class="o">=</span>mastodon.example.com <span class="c">#This is optional and only needed if your instance is hosted on a subdomain, but you want the instance to be known by its domain.</span>
<span class="nv">SINGLE_USER_MODE</span><span class="o">=</span><span class="nb">true</span> <span class="c">#Optional, only needed if you're hosting for just yourself</span>

<span class="c"># Sending mail</span>
<span class="c"># ------------</span>
<span class="nv">SMTP_SERVER</span><span class="o">=</span>&lt;smtp host&gt;
<span class="nv">SMTP_PORT</span><span class="o">=</span>587
<span class="nv">SMTP_LOGIN</span><span class="o">=</span>&lt;username&gt;
<span class="nv">SMTP_PASSWORD</span><span class="o">=</span>&lt;password&gt;
<span class="nv">SMTP_FROM_ADDRESS</span><span class="o">=</span>&lt;address the mastodon instance will email from&gt;
</code></pre></div></div>

<p>Move <code class="language-plaintext highlighter-rouge">PostgreSQL</code> block to a <code class="language-plaintext highlighter-rouge">db.env</code> file so you can share with the postgres container.</p>

<p><code class="language-plaintext highlighter-rouge">db.env</code></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># PostgreSQL</span>
<span class="c"># There might be some fancy way to de-dupe this, i just sent it.</span>
<span class="c"># ----------</span>
<span class="nv">DB_HOST</span><span class="o">=</span>db
<span class="nv">DB_USER</span><span class="o">=</span>mastodon
<span class="nv">DB_NAME</span><span class="o">=</span>mastodon_production
<span class="nv">DB_PASS</span><span class="o">=</span>&lt;generated password&gt;
<span class="nv">DB_PORT</span><span class="o">=</span>5432

<span class="c"># Needed so that the pgsql container creates the expected roles and dbs for you</span>
<span class="nv">POSTGRES_DB</span><span class="o">=</span>mastodon_production
<span class="nv">POSTGRES_USER</span><span class="o">=</span>mastodon
<span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>&lt;generated password&gt;
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker-compose.yml</code> (sourced from <a href="https://github.com/mastodon/mastodon/blob/main/docker-compose.yml">github</a> and edited)</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">db</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:14-alpine</span>
    <span class="na">shm_size</span><span class="pi">:</span> <span class="s">256mb</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">internal_network</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/mastodon/postgres14:/var/lib/postgresql/data</span>
    <span class="c1"># added from the default, otherwise you'll need to manually create a `mastodon` db and user/role</span>
    <span class="na">env_file</span><span class="pi">:</span> <span class="s">db.env</span>  
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">POSTGRES_HOST_AUTH_METHOD=trust'</span>
  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">redis:7-alpine</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">internal_network</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">CMD'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">redis-cli'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">ping'</span><span class="pi">]</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/mastodon/redis:/data</span>

  <span class="na">web</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">tootsuite/mastodon:v3.5.3</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">env_file</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.env.production</span>
      <span class="pi">-</span> <span class="s">db.env</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
      <span class="pi">-</span> <span class="s">internal_network</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="c1"># prettier-ignore</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">CMD-SHELL'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">wget</span><span class="nv"> </span><span class="s">-q</span><span class="nv"> </span><span class="s">--spider</span><span class="nv"> </span><span class="s">--proxy=off</span><span class="nv"> </span><span class="s">localhost:3000/health</span><span class="nv"> </span><span class="s">||</span><span class="nv"> </span><span class="s">exit</span><span class="nv"> </span><span class="s">1'</span><span class="pi">]</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">db</span>
      <span class="pi">-</span> <span class="s">redis</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/mastodon/public/system:/mastodon/public/system</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonweb.rule=Host(`example.com`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonweb.entrypoints=&lt;https-entry-point&gt;</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonweb.tls.certresolver=letsencrypttls</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.mastodonweb.loadbalancer.server.port=3000</span>

  <span class="na">streaming</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">tootsuite/mastodon:v3.5.3</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">env_file</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.env.production</span>
      <span class="pi">-</span> <span class="s">db.env</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">node ./streaming</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
      <span class="pi">-</span> <span class="s">internal_network</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="c1"># prettier-ignore</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">CMD-SHELL'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">wget</span><span class="nv"> </span><span class="s">-q</span><span class="nv"> </span><span class="s">--spider</span><span class="nv"> </span><span class="s">--proxy=off</span><span class="nv"> </span><span class="s">localhost:4000/api/v1/streaming/health</span><span class="nv"> </span><span class="s">||</span><span class="nv"> </span><span class="s">exit</span><span class="nv"> </span><span class="s">1'</span><span class="pi">]</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">db</span>
      <span class="pi">-</span> <span class="s">redis</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonstreaming.rule=(Host(`example.com`) &amp;&amp; PathPrefix(`/api/v1/streaming`))</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonstreaming.entrypoints=&lt;https-entry-point&gt;</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.mastodonstreaming.tls.certresolver=letsencrypttls</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.mastodonstreaming.loadbalancer.server.port=4000</span>


  <span class="na">sidekiq</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">tootsuite/mastodon:v3.5.3</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">env_file</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.env.production</span>
      <span class="pi">-</span> <span class="s">db.env</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bundle exec sidekiq</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">db</span>
      <span class="pi">-</span> <span class="s">redis</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
      <span class="pi">-</span> <span class="s">internal_network</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/mnt/user/appdata/mastodon/public/system:/mastodon/public/system</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">CMD-SHELL'</span><span class="pi">,</span> <span class="s2">"</span><span class="s">ps</span><span class="nv"> </span><span class="s">aux</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">grep</span><span class="nv"> </span><span class="s">'[s]idekiq</span><span class="se">\ </span><span class="s">6'</span><span class="nv"> </span><span class="s">||</span><span class="nv"> </span><span class="s">false"</span><span class="pi">]</span>


<span class="na">networks</span><span class="pi">:</span>
  <span class="c1">#the network that your traefik instance has access to</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">internal_network</span><span class="pi">:</span>
    <span class="na">internal</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Run the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose run <span class="nt">--rm</span> web rake secret  <span class="c">#copy to SECRET_KEY_BASE in .env.production</span>
docker-compose run <span class="nt">--rm</span> web rake secret  <span class="c">#copy to OTP_SECRET in .env.production</span>
docker-compose run <span class="nt">--rm</span> web bundle <span class="nb">exec </span>rake mastodon:webpush:generate_vapid_key <span class="c">#copy to VAPID_* keys</span>
docker-compose run <span class="nt">--rm</span> web bundle <span class="nb">exec </span>rake db:migrate
docker-compose run <span class="nt">--rm</span> web bin/tootctl accounts create &lt;username&gt; <span class="nt">--email</span><span class="o">=</span>&lt;email&gt;
docker-compose run <span class="nt">--rm</span> web bin/tootctl accounts modify &lt;username&gt; <span class="nt">--confirm</span> <span class="nt">--approve</span> <span class="nt">--enable</span> <span class="nt">--role</span><span class="o">=</span>admin
</code></pre></div></div>

<h3 id="draw-the-rest-of-the-owl">Draw the rest of the owl:</h3>
<p>Mastodon needs a <a href="https://docs.joinmastodon.org/spec/webfinger/">webfinger</a> to resolve links across the fediverse.
Setting this up will be different based on your infrastructure. I ended up using netlify to host my jekyll website and setup redirects via that.</p>

<p><code class="language-plaintext highlighter-rouge">_redirects</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/.well-known/webfinger	https://$(WEB_DOMAIN)/.well-known/webfinger
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">_config.yml</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>include:
  - '_redirects'
</code></pre></div></div>

<p>On Netlify make sure your bare domain is the primary domain, and that Netlify is serving your SSL certs via Let’s Encrypt.</p>

<p>On my domain registrar, I’ve setup an <code class="language-plaintext highlighter-rouge">A</code> Record pointing my <code class="language-plaintext highlighter-rouge">WEB_DOMAIN</code> subdomain to my instance’s IP address.</p>

<h3 id="test-webfinger">Test webfinger</h3>

<p>Test that your instance’s web finger is setup right.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-vL</span> https://example.com/.well-known/webfinger?resource<span class="o">=</span>acct:username@example.com
</code></pre></div></div>

<p>Will return json with redirects.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* Connection #1 to host mastodon.example.com left intact
{"subject":"acct:username@example.com","aliases":["https://mastodon.example.com/@username","https://mastodon.example.com/users/username"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://mastodon.example.com/@username"},{"rel":"self","type":"application/activity+json","href":"https://mastodon.example.com/users/username"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://mastodon.example.com/authorize_interaction?uri={uri}"}]}%
</code></pre></div></div>

<h3 id="updating-to-402">Updating to 4.0.2</h3>

<p>Here’s how I updated my instance to 4.0.2:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose exec db pg_dump -Fc -U mastodon mastodon_production &gt; 3-5-3.dump
</code></pre></div></div>

<p>Update usages of <code class="language-plaintext highlighter-rouge">tootsuite/mastodon:v3.5.3</code> to <code class="language-plaintext highlighter-rouge">toosuite/mastodon:v4.0.2</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose pull
docker-compose run --rm -e SKIP_POST_DEPLOYMENT_MIGRATIONS=true web rails db:migrate
docker-compose run --rm web rails db:migrate
docker-compose up -d
</code></pre></div></div>

<h3 id="generally-updating">Generally updating</h3>

<p>Update all tootsuite versions to newest</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose down
docker-compose pull
docker-compose run --rm web bundle exec rake db:migrate
docker-compose run --rm web bundle exec rake assets:precompile
docker-compose up -d
</code></pre></div></div>

<p>Shoutout to <a href="https://mastodon.jakewharton.com/@jw">Jake</a> for the help and quick review.</p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[(This assumes you have a running traefik instance)]]></summary></entry><entry><title type="html">Using Kotlin Extensions for Rx-ifying</title><link href="https://vishnurajeevan.com/using-kotlin-for-rxifying-extensions/" rel="alternate" type="text/html" title="Using Kotlin Extensions for Rx-ifying" /><published>2016-02-13T00:00:00+00:00</published><updated>2016-02-13T00:00:00+00:00</updated><id>https://vishnurajeevan.com/using-kotlin-for-rxifying-extensions</id><content type="html" xml:base="https://vishnurajeevan.com/using-kotlin-for-rxifying-extensions/"><![CDATA[<p>Kotlin and Reactive Extensions (Rx) are the new hotness in Android development, and not without reason. Both technologies are loved for being concise, expressive and powerful. This is especially useful in the Android world where APIs can be long-winded and filled with ceremony.</p>

<p>One of the main features that I’ve fallen in love with while using Kotlin is class extensions (docs). This feature comes in handy quite often when working with APIs that are not yet RxJava compatible in a project that is using RxJava extensively.</p>

<p>Let’s take a look at a simple example when using the SlidingUpPanel library.
Basic API Usage</p>

<p>One of the things you might want to do when using this library is listen for panel events such as expanding and collapsing of the panel.</p>

<p>When doing this without RxJava you get the following codeblock</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">slidingUpPanel</span><span class="p">.</span><span class="nf">setPanelSlideListener</span><span class="p">(</span><span class="k">object</span><span class="p">:</span> <span class="nc">SlidingUpPanelLayout</span><span class="p">.</span><span class="nc">PanelSlideListener</span> <span class="p">{</span>
  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelExpanded</span><span class="p">(</span><span class="n">p0</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">{</span>
  <span class="p">}</span>

  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelSlide</span><span class="p">(</span><span class="n">p0</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?,</span> <span class="n">p1</span> <span class="p">:</span> <span class="nc">Float</span><span class="p">)</span> <span class="p">{</span>
  <span class="p">}</span>

  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelCollapsed</span><span class="p">(</span><span class="n">p0</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">{</span>
  <span class="p">}</span>

  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelHidden</span><span class="p">(</span><span class="n">p0</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">{</span>
  <span class="p">}</span>

  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelAnchored</span><span class="p">(</span><span class="n">p0</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">{</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>This is a required implementation when you might only care about the expanded and collapsed state.
Kotlin Extension</p>

<p>So, lets move this into a Kotlin extension:</p>

<p>We’re going to start with a data class to model the panel events:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="kd">class</span> <span class="nc">PanelEvent</span> <span class="p">{</span> <span class="nc">COLLAPSED</span><span class="p">,</span> <span class="nc">EXPANDED</span><span class="p">,</span> <span class="nc">HIDDEN</span><span class="p">,</span> <span class="nc">ANCHORED</span><span class="p">,</span> <span class="nc">SLIDE</span> <span class="p">}</span>
<span class="kd">data class</span> <span class="nc">PanelData</span><span class="p">(</span><span class="kd">val</span> <span class="py">event</span> <span class="p">:</span> <span class="nc">PanelEvent</span><span class="p">,</span> <span class="kd">val</span> <span class="py">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?,</span> <span class="kd">val</span> <span class="py">slideOffset</span> <span class="p">:</span> <span class="nc">Float</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
</code></pre></div></div>

<p>Now the actual class extension:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">SlidingUpPanelLayout</span><span class="p">.</span><span class="nf">panelSlides</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Observable</span><span class="p">&lt;</span><span class="nc">PanelData</span><span class="p">&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nc">Observable</span><span class="p">.</span><span class="n">defer</span><span class="p">&lt;</span><span class="nc">PanelData</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="nc">Observable</span><span class="p">.</span><span class="nf">create</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(!</span><span class="n">it</span><span class="p">.</span><span class="n">isUnsubscribed</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">setPanelSlideListener</span><span class="p">(</span><span class="kd">object</span> <span class="err">: </span><span class="nc">SlidingUpPanelLayout</span><span class="p">.</span><span class="nc">PanelSlideListener</span> <span class="p">{</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelSlide</span><span class="p">(</span><span class="n">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?,</span> <span class="n">slideOffset</span> <span class="p">:</span> <span class="nc">Float</span><span class="p">)</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="nf">onNext</span><span class="p">(</span><span class="nc">PanelData</span><span class="p">(</span><span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">SLIDE</span><span class="p">,</span> <span class="n">panel</span><span class="p">,</span> <span class="n">slideOffset</span><span class="p">))</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelExpanded</span><span class="p">(</span><span class="n">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="nf">onNext</span><span class="p">(</span><span class="nc">PanelData</span><span class="p">(</span><span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">EXPANDED</span><span class="p">,</span> <span class="n">panel</span><span class="p">))</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelCollapsed</span><span class="p">(</span><span class="n">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="nf">onNext</span><span class="p">(</span><span class="nc">PanelData</span><span class="p">(</span><span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">COLLAPSED</span><span class="p">,</span> <span class="n">panel</span><span class="p">))</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelHidden</span><span class="p">(</span><span class="n">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="nf">onNext</span><span class="p">(</span><span class="nc">PanelData</span><span class="p">(</span><span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">HIDDEN</span><span class="p">,</span> <span class="n">panel</span><span class="p">))</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onPanelAnchored</span><span class="p">(</span><span class="n">panel</span> <span class="p">:</span> <span class="nc">View</span><span class="p">?)</span> <span class="p">=</span> <span class="n">it</span><span class="p">.</span><span class="nf">onNext</span><span class="p">(</span><span class="nc">PanelData</span><span class="p">(</span><span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">ANCHORED</span><span class="p">,</span> <span class="n">panel</span><span class="p">))</span>
        <span class="p">})</span>

        <span class="n">it</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="kd">object</span> <span class="err">: </span><span class="nc">MainThreadSubscription</span><span class="p">()</span> <span class="p">{</span>
          <span class="c1">//we should null the listener when the subscriber unsubscribes</span>
          <span class="k">override</span> <span class="k">fun</span> <span class="nf">onUnsubscribe</span><span class="p">()</span> <span class="p">=</span> <span class="nf">setPanelSlideListener</span><span class="p">(</span><span class="k">null</span><span class="p">)</span>
        <span class="p">})</span>
      <span class="p">}</span>
  <span class="p">}}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And the calling code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">slidingUpPanel</span><span class="p">.</span><span class="nf">panelSlides</span><span class="p">()</span>
  <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">event</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">filter</span> <span class="p">{</span><span class="n">it</span> <span class="p">==</span> <span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">COLLAPSED</span> <span class="p">||</span> <span class="n">it</span> <span class="p">==</span> <span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">EXPANDED</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">subscribe</span> <span class="p">{</span>
    <span class="nc">Timber</span><span class="p">.</span><span class="nf">d</span><span class="p">(</span><span class="s">"panel state changed"</span><span class="p">)</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Now you have the power of RxJava’s composibility and concise syntax availble to you, without losing the context of the action itself.
Java Interop</p>

<p>Because of Kotlin and Java’s interoperability we can also use this extension from Java code as a “Util” class.</p>

<p>Add the following to the top of the extension file (otherwise it’ll use <FileName>Kt as the classname)</FileName></p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="n">file</span><span class="p">:</span><span class="nc">JvmName</span><span class="p">(</span><span class="s">"SlidingUpPanelLayoutUtils"</span><span class="p">)</span>
</code></pre></div></div>

<p>And now from java code you can do the following</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">SlidingUpPanelLayoutUtils</span><span class="p">.</span><span class="nf">panelSlides</span><span class="p">(</span><span class="n">slidingUpPanelLayout</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">new</span> <span class="nc">Func1</span><span class="p">&lt;</span><span class="nc">PanelData</span><span class="p">,</span> <span class="nc">PanelEvent</span><span class="p">&gt;()</span> <span class="p">{</span>
        <span class="nd">@Override</span>
        <span class="k">public</span> <span class="nc">PanelEvent</span> <span class="nf">call</span><span class="p">(</span><span class="nc">PanelData</span> <span class="n">panelData</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="n">panelData</span><span class="p">.</span><span class="nf">getEvent</span><span class="p">();</span>
        <span class="p">}</span>
      <span class="p">})</span>
      <span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="n">new</span> <span class="nc">Func1</span><span class="p">&lt;</span><span class="nc">PanelEvent</span><span class="p">,</span> <span class="nc">Boolean</span><span class="p">&gt;()</span> <span class="p">{</span>
        <span class="nd">@Override</span>
        <span class="k">public</span> <span class="nc">Boolean</span> <span class="nf">call</span><span class="p">(</span><span class="nc">PanelEvent</span> <span class="n">panelEvent</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="n">panelEvent</span> <span class="p">==</span> <span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">COLLAPSED</span> <span class="p">||</span> <span class="n">panelEvent</span> <span class="p">==</span> <span class="nc">PanelEvent</span><span class="p">.</span><span class="nc">EXPANDED</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">})</span>
      <span class="p">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">new</span> <span class="nc">Action1</span><span class="p">&lt;</span><span class="nc">PanelEvent</span><span class="p">&gt;()</span> <span class="p">{</span>
        <span class="nd">@Override</span>
        <span class="k">public</span> <span class="n">void</span> <span class="nf">call</span><span class="p">(</span><span class="nc">PanelEvent</span> <span class="n">panelEvent</span><span class="p">)</span> <span class="p">{</span>
          <span class="nc">Timber</span><span class="p">.</span><span class="nf">d</span><span class="p">(</span><span class="s">"panel state changed"</span><span class="p">);</span>
        <span class="p">}</span>
      <span class="p">});</span>
</code></pre></div></div>

<p>For more examples, checkout RxBinding which has extensions for Android framework classes.</p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[Kotlin and Reactive Extensions (Rx) are the new hotness in Android development, and not without reason. Both technologies are loved for being concise, expressive and powerful. This is especially useful in the Android world where APIs can be long-winded and filled with ceremony.]]></summary></entry><entry><title type="html">Using RecyclerView With Multiple Items</title><link href="https://vishnurajeevan.com/using-recyclerview/" rel="alternate" type="text/html" title="Using RecyclerView With Multiple Items" /><published>2015-07-12T00:00:00+00:00</published><updated>2015-07-12T00:00:00+00:00</updated><id>https://vishnurajeevan.com/using-recyclerview</id><content type="html" xml:base="https://vishnurajeevan.com/using-recyclerview/"><![CDATA[<p>A simple pain point I always seemed to encounter when developing for Android is dealing with multiple items types in a ListView or a GridView. Luckily, RecyclerView has some pretty neat built in support for this feature, and I’ve used it successfully with collections containing &gt;10 item types. Adding that kind of complexity can lead to lots of ugly code in your adapter, and trying to keep that clean will help in maintainability and reduce possible bugs. Let’s build a quick app that shows various fruits and veggies with specific styling for their type.</p>

<p>First off, lets start with the crux of the implementation: the Adapter’s item model.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">FruitVegItem</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">SOURCE</span><span class="o">)</span>
    <span class="nd">@IntDef</span><span class="o">({</span><span class="no">FRUIT</span><span class="o">,</span> <span class="no">VEGETABLE</span><span class="o">})</span>
    <span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">ViewType</span> <span class="o">{</span>
    <span class="o">}</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">FRUIT</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">VEGETABLE</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>

    <span class="no">T</span> <span class="n">object</span><span class="o">;</span>
    <span class="nd">@ViewType</span> <span class="kt">int</span> <span class="n">viewType</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">FruitVegItem</span><span class="o">(</span><span class="no">T</span> <span class="n">object</span><span class="o">,</span> <span class="kt">int</span> <span class="n">viewType</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">object</span> <span class="o">=</span> <span class="n">object</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">viewType</span> <span class="o">=</span> <span class="n">viewType</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Using this object, we can wrap the item’s model and have it specify it’s item type for the adapter.</p>

<p>Next, let’s use it in the adapter:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">FruitVegItem</span><span class="o">&gt;</span> <span class="n">list</span><span class="o">;</span>

<span class="o">...</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">addFruit</span><span class="o">(</span><span class="nc">String</span> <span class="n">fruitName</span><span class="o">){</span>
        <span class="n">list</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">FruitVegItem</span><span class="o">&lt;&gt;(</span><span class="n">fruitName</span><span class="o">,</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">FRUIT</span><span class="o">));</span>
        <span class="n">notifyItemInserted</span><span class="o">(</span><span class="n">list</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
    <span class="o">}</span>

<span class="o">...</span>
</code></pre></div></div>

<p>By exposing a simple API for each item type, the adapter can control how and where each item is added into it’s backing collection.</p>

<h3 id="viewholders">ViewHolders</h3>

<p>Since we have different items, it makes sense to have different item ViewHolder objects and layouts.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">FruitViewHolder</span> <span class="kd">extends</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span><span class="o">{</span>

    <span class="kd">public</span> <span class="nc">TextView</span> <span class="n">name</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">FruitViewHolder</span><span class="o">(</span><span class="nc">View</span> <span class="n">itemView</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">itemView</span><span class="o">);</span>
        <span class="n">name</span> <span class="o">=</span> <span class="o">(</span><span class="nc">TextView</span><span class="o">)</span> <span class="n">itemView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">name</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">FruitViewHolder</span> <span class="nf">create</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">ViewGroup</span> <span class="n">parent</span><span class="o">){</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">FruitViewHolder</span><span class="o">(</span><span class="nc">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">context</span><span class="o">).</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">item_fruit_veg</span><span class="o">,</span> <span class="n">parent</span><span class="o">,</span> <span class="kc">false</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">bind</span><span class="o">(</span><span class="nc">FruitViewHolder</span> <span class="n">holder</span><span class="o">,</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">name</span><span class="o">){</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">itemView</span><span class="o">.</span><span class="na">setBackgroundResource</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">fruit_bg</span><span class="o">);</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">name</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
              <span class="na">android:orientation=</span><span class="s">"vertical"</span>
              <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
              <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;TextView</span>
        <span class="na">android:id=</span><span class="s">"@+id/name"</span>
        <span class="na">android:gravity=</span><span class="s">"center"</span>
        <span class="na">android:padding=</span><span class="s">"8dp"</span>
        <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
        <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">/&gt;</span>

<span class="nt">&lt;/LinearLayout&gt;</span>
</code></pre></div></div>

<p>The key in the ViewHolder object is that it exposes static create and bind method calls. The create method is just a static factory method, a common practice in Java. The bind method, however, allows use to abstract all ViewHolder binding logic into the object itself, rather than polluting the adapter’s #onBindViewHolder method.</p>

<h3 id="adapter">Adapter</h3>

<p>Now that we’ve setup the wrapper object and the viewholder logic, lets take a look at what the adapter has to do to button all this up.</p>

<p>First off, make sure you override the RecyclerView.Adapter#getItemViewType method. This method lets the adapter know to expect multiple view types and not attempt to use a recycled ViewHolder of the wrong type.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">...</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="nf">getItemViewType</span><span class="o">(</span><span class="kt">int</span> <span class="n">position</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">list</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">position</span><span class="o">).</span><span class="na">viewType</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">...</span>
</code></pre></div></div>

<p>Finally, the adapter needs to actually create and bind the view holders:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span> <span class="nf">onCreateViewHolder</span><span class="o">(</span><span class="nc">ViewGroup</span> <span class="n">viewGroup</span><span class="o">,</span> <span class="kt">int</span> <span class="n">viewType</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">switch</span> <span class="o">(</span><span class="n">viewType</span><span class="o">){</span>
        <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">FRUIT</span><span class="o">:</span>
            <span class="k">return</span> <span class="nc">FruitViewHolder</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">viewGroup</span><span class="o">);</span>
        <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">VEGETABLE</span><span class="o">:</span>
            <span class="k">return</span> <span class="nc">VegViewHolder</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">viewGroup</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>

<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onBindViewHolder</span><span class="o">(</span><span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span> <span class="n">viewHolder</span><span class="o">,</span> <span class="kt">int</span> <span class="n">position</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">switch</span> <span class="o">(</span><span class="n">getItemViewType</span><span class="o">(</span><span class="n">position</span><span class="o">)){</span>
        <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">FRUIT</span><span class="o">:</span>
            <span class="nc">FruitViewHolder</span><span class="o">.</span><span class="na">bind</span><span class="o">((</span><span class="nc">FruitViewHolder</span><span class="o">)</span> <span class="n">viewHolder</span><span class="o">,</span>
                                 <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">list</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">position</span><span class="o">).</span><span class="na">object</span><span class="o">,</span>
                                 <span class="n">listener</span><span class="o">);</span>
            <span class="k">break</span><span class="o">;</span>
        <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">VEGETABLE</span><span class="o">:</span>
            <span class="nc">VegViewHolder</span><span class="o">.</span><span class="na">bind</span><span class="o">((</span><span class="nc">VegViewHolder</span><span class="o">)</span> <span class="n">viewHolder</span><span class="o">,</span>
                               <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">list</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">position</span><span class="o">).</span><span class="na">object</span><span class="o">,</span>
                               <span class="n">listener</span><span class="o">);</span>
            <span class="k">break</span><span class="o">;</span>

    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="finale">Finale</h3>

<p>Now you get to use it:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span><span class="o">[]</span> <span class="n">fruits</span> <span class="o">=</span> <span class="n">getResources</span><span class="o">().</span><span class="na">getStringArray</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">array</span><span class="o">.</span><span class="na">fruits</span><span class="o">);</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">veggies</span> <span class="o">=</span> <span class="n">getResources</span><span class="o">().</span><span class="na">getStringArray</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">array</span><span class="o">.</span><span class="na">vegetables</span><span class="o">);</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">veg</span> <span class="o">:</span> <span class="n">veggies</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">adapter</span><span class="o">.</span><span class="na">addVeg</span><span class="o">(</span><span class="n">veg</span><span class="o">);</span>
<span class="o">}</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">fruit</span> <span class="o">:</span> <span class="n">fruits</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">adapter</span><span class="o">.</span><span class="na">addFruit</span><span class="o">(</span><span class="n">fruit</span><span class="o">);</span>
<span class="o">}</span>

<span class="n">recyclerView</span><span class="o">.</span><span class="na">setAdapter</span><span class="o">(</span><span class="n">adapter</span><span class="o">);</span>
</code></pre></div></div>

<p>What it ends up looking like:
<img src="http://burntcookie90.github.io/images/multi_item_recyclerview_list.png" alt="image" /></p>

<p>From here, adding new item types is quite simple:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Add a ViewType to the FruitVegItem
Create your ViewHolder and layout
Hook it all up in the adapter
</code></pre></div></div>

<h3 id="encore">Encore</h3>

<p>When you start using viewtypes like this, you can create some pretty fun, complicated layouts easily via the GridLayoutManager</p>

<p>For example:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gridLayoutManager</span><span class="o">.</span><span class="na">setSpanSizeLookup</span><span class="o">(</span><span class="k">new</span> <span class="nc">GridLayoutManager</span><span class="o">.</span><span class="na">SpanSizeLookup</span><span class="o">()</span> <span class="o">{</span>
          <span class="nd">@Override</span>
          <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getSpanSize</span><span class="o">(</span><span class="kt">int</span> <span class="n">position</span><span class="o">)</span> <span class="o">{</span>
              <span class="k">switch</span><span class="o">(</span><span class="n">adapter</span><span class="o">.</span><span class="na">getItemViewType</span><span class="o">(</span><span class="n">position</span><span class="o">)){</span>
                  <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">FRUIT</span><span class="o">:</span>
                      <span class="k">return</span> <span class="mi">1</span><span class="o">;</span>
                  <span class="k">case</span> <span class="nc">FruitVegItem</span><span class="o">.</span><span class="na">VEGETABLE</span><span class="o">:</span>
                      <span class="k">return</span> <span class="mi">3</span><span class="o">;</span>
              <span class="o">}</span>
              <span class="k">return</span> <span class="mi">0</span><span class="o">;</span>
          <span class="o">}</span>
      <span class="o">});</span>
</code></pre></div></div>

<p><img src="http://burntcookie90.github.io/images/multi_item_recyclerview_grid.png" alt="image" /></p>

<p>You can view the full code (plus click listener implementations) on <a href="https://github.com/burntcookie90/multiitemrecyclerview/">github</a></p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[A simple pain point I always seemed to encounter when developing for Android is dealing with multiple items types in a ListView or a GridView. Luckily, RecyclerView has some pretty neat built in support for this feature, and I’ve used it successfully with collections containing &gt;10 item types. Adding that kind of complexity can lead to lots of ugly code in your adapter, and trying to keep that clean will help in maintainability and reduce possible bugs. Let’s build a quick app that shows various fruits and veggies with specific styling for their type.]]></summary></entry><entry><title type="html">Rebound for Android View Animations</title><link href="https://vishnurajeevan.com/rebound/" rel="alternate" type="text/html" title="Rebound for Android View Animations" /><published>2014-08-06T00:00:00+00:00</published><updated>2014-08-06T00:00:00+00:00</updated><id>https://vishnurajeevan.com/rebound</id><content type="html" xml:base="https://vishnurajeevan.com/rebound/"><![CDATA[<p>So after seeing this <a href="http://www.reddit.com/r/androiddev/comments/2cfcdl/demo_app_for_using_facebooks_rebound_animation/">post</a> on reddit the other day, I decided I’d try and utilize Rebound for the link drawer in my Hacker News app. Currently, the app uses a basic translate animation on the <a href="https://github.com/dinosaurwithakatana/holo_hacker_news/blob/9b6324cb9611058d99c35232e008d3935536c535/app/src/main/java/io/dwak/holohackernews/app/StoryLinkFragment.java">StoryLinkFragment</a> when the “Show Link” button is pressed.</p>

<p>The simple translate animation is as follows:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;set</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
     <span class="na">android:shareInterpolator=</span><span class="s">"false"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;translate</span> <span class="na">android:fromXDelta=</span><span class="s">"0%"</span> <span class="na">android:toXDelta=</span><span class="s">"0%"</span>
               <span class="na">android:fromYDelta=</span><span class="s">"0%"</span> <span class="na">android:toYDelta=</span><span class="s">"-100%"</span>
               <span class="na">android:duration=</span><span class="s">"@integer/fragment_animation_times"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/set&gt;</span>
</code></pre></div></div>

<p>The animation ends up looking like this:</p>

<p><a href="http://zippy.gfycat.com/QualifiedImportantChimneyswift.webm">gfy</a></p>

<p>I consider this a bare minimum animation, gets the job done in showing the user a new view, but has no easing or natural motion to it.</p>

<p>Rebound handles this issue by utilizing spring physics on the views, as seen <a href="https://facebook.github.io/rebound/">here</a>. In order to improve the earlier animation, I created a <a href="https://developer.android.com/reference/android/widget/RelativeLayout.html">RelativeLayout</a> that has the spring physics built in.</p>

<p>The code for ReboundRevealRelativeLayout follows (it could probably use a bit of cleanup):</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">io.dwak.holohackernews.app.widget</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">android.content.Context</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">android.util.AttributeSet</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">android.widget.RelativeLayout</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.facebook.rebound.Spring</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.facebook.rebound.SpringConfig</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.facebook.rebound.SpringListener</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.facebook.rebound.SpringSystem</span><span class="o">;</span>

<span class="cm">/**
 * A RelativeLayout that can be animated vertically or horizontally using Facebook's Rebound library
 * Created by vishnu on 8/5/14.
 * @see android.widget.RelativeLayout
 */</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ReboundRevealRelativeLayout</span> <span class="kd">extends</span> <span class="nc">RelativeLayout</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">SpringConfig</span> <span class="no">SPRING_CONFIG</span> <span class="o">=</span> <span class="nc">SpringConfig</span><span class="o">.</span><span class="na">fromOrigamiTensionAndFriction</span><span class="o">(</span><span class="mi">6</span><span class="o">,</span> <span class="mi">6</span><span class="o">);</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">TRANSLATE_DIRECTION_VERTICAL</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">TRANSLATE_DIRECTION_HORIZONTAL</span><span class="o">=</span> <span class="mi">1</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kt">int</span> <span class="n">mRevealPixel</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">mStashPixel</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Spring</span> <span class="n">mSpring</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">mOpen</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">mTranslateDirection</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">RevealListener</span> <span class="n">mRevealListener</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">ReboundRevealRelativeLayout</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nf">ReboundRevealRelativeLayout</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">AttributeSet</span> <span class="n">attrs</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">attrs</span><span class="o">,</span> <span class="mi">0</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nf">ReboundRevealRelativeLayout</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">AttributeSet</span> <span class="n">attrs</span><span class="o">,</span> <span class="kt">int</span> <span class="n">defStyle</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">attrs</span><span class="o">,</span> <span class="n">defStyle</span><span class="o">);</span>
        <span class="nc">SpringSystem</span> <span class="n">springSystem</span> <span class="o">=</span> <span class="nc">SpringSystem</span><span class="o">.</span><span class="na">create</span><span class="o">();</span>
        <span class="n">mSpring</span> <span class="o">=</span> <span class="n">springSystem</span><span class="o">.</span><span class="na">createSpring</span><span class="o">();</span>
        <span class="n">mSpring</span><span class="o">.</span><span class="na">setSpringConfig</span><span class="o">(</span><span class="no">SPRING_CONFIG</span><span class="o">);</span>
        <span class="nc">LinkSpringListener</span> <span class="n">linkSpringListener</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkSpringListener</span><span class="o">();</span>
        <span class="n">mSpring</span><span class="o">.</span><span class="na">setCurrentValue</span><span class="o">(</span><span class="mi">0</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setEndValue</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
                <span class="o">.</span><span class="na">addListener</span><span class="o">(</span><span class="n">linkSpringListener</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Set whether the view visible or not
     *
     * @param open true if visible
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setOpen</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">open</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mOpen</span> <span class="o">=</span> <span class="n">open</span><span class="o">;</span>
        <span class="n">togglePosition</span><span class="o">(</span><span class="n">open</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">togglePosition</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">open</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mSpring</span><span class="o">.</span><span class="na">setEndValue</span><span class="o">(</span><span class="n">open</span>
                <span class="o">?</span> <span class="mi">0</span>
                <span class="o">:</span> <span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isOpen</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">mOpen</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Sets the direction in which to reveal and stash the view
     *
     * @param translateDirection {@link io.dwak.holohackernews.app.widget.ReboundRevealRelativeLayout.TranslateDirection} describing the direction to animate
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setTranslateDirection</span><span class="o">(</span><span class="kt">int</span> <span class="n">translateDirection</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mTranslateDirection</span> <span class="o">=</span> <span class="n">translateDirection</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">class</span> <span class="nc">LinkSpringListener</span> <span class="kd">implements</span> <span class="nc">SpringListener</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSpringUpdate</span><span class="o">(</span><span class="nc">Spring</span> <span class="n">spring</span><span class="o">)</span> <span class="o">{</span>
            <span class="kt">float</span> <span class="n">val</span> <span class="o">=</span> <span class="o">(</span><span class="kt">float</span><span class="o">)</span> <span class="n">spring</span><span class="o">.</span><span class="na">getCurrentValue</span><span class="o">();</span>
            <span class="kt">float</span> <span class="n">maxTranslate</span> <span class="o">=</span> <span class="n">mStashPixel</span><span class="o">;</span>
            <span class="kt">float</span> <span class="n">minTranslate</span> <span class="o">=</span> <span class="n">mRevealPixel</span><span class="o">;</span>
            <span class="kt">float</span> <span class="n">range</span> <span class="o">=</span> <span class="n">maxTranslate</span> <span class="o">-</span> <span class="n">minTranslate</span><span class="o">;</span>
            <span class="kt">float</span> <span class="n">translate</span> <span class="o">=</span> <span class="o">(</span><span class="n">val</span> <span class="o">*</span> <span class="n">range</span><span class="o">)</span> <span class="o">+</span> <span class="n">minTranslate</span><span class="o">;</span>

            <span class="k">switch</span> <span class="o">(</span><span class="n">mTranslateDirection</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">case</span> <span class="nl">TRANSLATE_DIRECTION_HORIZONTAL:</span>
                    <span class="n">setTranslationX</span><span class="o">(</span><span class="n">translate</span><span class="o">);</span>
                    <span class="k">break</span><span class="o">;</span>
                <span class="k">case</span> <span class="nl">TRANSLATE_DIRECTION_VERTICAL:</span>
                    <span class="n">setTranslationY</span><span class="o">(</span><span class="n">translate</span><span class="o">);</span>
                    <span class="k">break</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSpringAtRest</span><span class="o">(</span><span class="nc">Spring</span> <span class="n">spring</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">mRevealListener</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">mRevealListener</span><span class="o">.</span><span class="na">onVisibilityChange</span><span class="o">(</span><span class="n">spring</span><span class="o">.</span><span class="na">getCurrentValue</span><span class="o">()</span> <span class="o">==</span> <span class="mf">0.0</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSpringActivate</span><span class="o">(</span><span class="nc">Spring</span> <span class="n">spring</span><span class="o">)</span> <span class="o">{</span>

        <span class="o">}</span>

        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSpringEndStateChange</span><span class="o">(</span><span class="nc">Spring</span> <span class="n">spring</span><span class="o">)</span> <span class="o">{</span>

        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getRevealPixel</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">mRevealPixel</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getStashPixel</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">mStashPixel</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Sets the pixel to reveal the view to
     *
     * @param revealPixel Integer value to set the view to when revealing
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setRevealPixel</span><span class="o">(</span><span class="kt">int</span> <span class="n">revealPixel</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mRevealPixel</span> <span class="o">=</span> <span class="n">revealPixel</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Sets the pixel to stash the view to
     *
     * @param stashPixel Integer value to set the view to when stashing
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setStashPixel</span><span class="o">(</span><span class="kt">int</span> <span class="n">stashPixel</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mStashPixel</span> <span class="o">=</span> <span class="n">stashPixel</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Set a listener callback for when visibility animations are complete
     *
     * @param listener {@link io.dwak.holohackernews.app.widget.ReboundRevealRelativeLayout.RevealListener}  listener for when animations complete
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setRevealListener</span><span class="o">(</span><span class="nc">RevealListener</span> <span class="n">listener</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">mRevealListener</span> <span class="o">=</span> <span class="n">listener</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="cm">/**
     * Interface to implement if you want to subscribe to visibility changes
     */</span>
    <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">RevealListener</span> <span class="o">{</span>
        <span class="kt">void</span> <span class="nf">onVisibilityChange</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">visible</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Using this layout for the link view ends up looking like this:</p>

<p><a href="http://zippy.gfycat.com/FavoriteFarHummingbird.webm">gfy</a></p>

<p>Comparing the two, it’s easy to see the improvement in the motion on the drawer reveal. It’s a simple animation but the little things count towards creating a pleasing user experience.</p>

<p>The fragment code that controls this view can be found [here].</p>]]></content><author><name>Vishnu Rajeevan</name></author><summary type="html"><![CDATA[So after seeing this post on reddit the other day, I decided I’d try and utilize Rebound for the link drawer in my Hacker News app. Currently, the app uses a basic translate animation on the StoryLinkFragment when the “Show Link” button is pressed.]]></summary></entry></feed>