Jekyll2022-12-21T07:10:35+00:00https://plusmobileapps.com/feed.xmlPlus Mobile AppsPlace to learn how to build great mobile apps.
Andrew Steinmetzandrew@plusmobileappsHow to use Firebase Authentication with Ktor 2.02022-08-02T00:00:00+00:002022-08-02T00:00:00+00:00https://plusmobileapps.com/2022/08/02/ktor-firebase-auth<p><img src="/assets/images/ktor-firebase-authentication.png" alt="" /></p>
<p>With the release of Ktor 2.0, one of the migrations I had to do was for Firebase Authentication which I first learned about how to use with Ktor 1.6 from this <a href="https://levelup.gitconnected.com/how-to-integrate-firebase-authentication-with-ktors-auth-feature-dc2c3893a0cc">medium article</a> last year. Learn how to setup Firebase Authentication with Ktor 2.0 and how to test it.</p>
<!--more-->
<h2 id="project-setup">Project Setup</h2>
<h3 id="firebase-project-setup">Firebase Project Setup</h3>
<p>Before downloading the starter project, follow these <a href="https://cloud.google.com/firestore/docs/client/get-firebase">instructions</a> to create a new firebase project and enable authentication. Then click on the settings button in the side bar -> project settings -> service accounts tab -> generate a new private key which should then download a JSON file to your machine.</p>
<p><img src="/assets/images/firebase-console-project-settings.png" alt="" /></p>
<p><img src="/assets/images/firebase-console-service-account.png" alt="" /></p>
<h3 id="download-and-configure-project">Download and Configure Project</h3>
<p>Then download the <a href="https://start.ktor.io#/settings?name=ktor-firebase-admin-sample&website=plusmobileapps.com&artifact=com.plusmobileapps.ktor-firebase-admin-sample&kotlinVersion=1.7.10&ktorVersion=2.0.3&buildSystem=GRADLE_KTS&engine=NETTY&configurationIn=CODE&addSampleCode=true&plugins=content-negotiation%2Crouting%2Ckotlinx-serialization%2Cauth">Ktor Project Template</a> from the Ktor Project Generator site. It will setup Ktor with 2.0.3 and the following plugins:</p>
<ul>
<li>Authentication</li>
<li>Content Negotiation</li>
<li>kotlinx.serialization</li>
<li>Routing</li>
</ul>
<p>Now with the JSON file downloaded from the service account creation, rename this file to <code class="language-plaintext highlighter-rouge">ktor-firebase-auth-adminsdk.json</code> and move it into this project under <code class="language-plaintext highlighter-rouge">src/main/resources/ktor-firebase-auth-adminsdk.json</code></p>
<p class="warning">The service account JSON configuration should not be checked into your git repository as this should be kept secret. To prevent this, add the file <code class="language-plaintext highlighter-rouge">src/main/resources/ktor-firebase-auth-adminsdk.json</code> to your <code class="language-plaintext highlighter-rouge">.gitignore</code> file.</p>
<p>Finally add the <a href="https://firebase.google.com/docs/admin/setup#add-sdk">Firebase Admin Java SDK</a> to the <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> file in order to user Firebase Authentication.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">dependencies</span> <span class="p">{</span>
<span class="nf">implementation</span><span class="p">(</span><span class="s">"com.google.firebase:firebase-admin:9.0.0"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="setup-firebase-app">Setup Firebase App</h2>
<p>With the project configured, the FirebaseApp on the server must be initialized using the service account JSON file placed in the resources folder.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">object</span> <span class="nc">FirebaseAdmin</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">serviceAccount</span><span class="p">:</span> <span class="nc">InputStream</span><span class="p">?</span> <span class="p">=</span>
<span class="k">this</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">.</span><span class="n">classLoader</span><span class="p">.</span><span class="nf">getResourceAsStream</span><span class="p">(</span><span class="s">"ktor-firebase-auth-adminsdk.json"</span><span class="p">)</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">options</span><span class="p">:</span> <span class="nc">FirebaseOptions</span> <span class="p">=</span> <span class="nc">FirebaseOptions</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
<span class="p">.</span><span class="nf">setCredentials</span><span class="p">(</span><span class="nc">GoogleCredentials</span><span class="p">.</span><span class="nf">fromStream</span><span class="p">(</span><span class="n">serviceAccount</span><span class="p">))</span>
<span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="k">fun</span> <span class="nf">init</span><span class="p">():</span> <span class="nc">FirebaseApp</span> <span class="p">=</span> <span class="nc">FirebaseApp</span><span class="p">.</span><span class="nf">initializeApp</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then simply call the <code class="language-plaintext highlighter-rouge">init()</code> function when the server is first started.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">embeddedServer</span><span class="p">(</span><span class="nc">Netty</span><span class="p">,</span> <span class="n">port</span> <span class="p">=</span> <span class="mi">8080</span><span class="p">,</span> <span class="n">host</span> <span class="p">=</span> <span class="s">"0.0.0.0"</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">FirebaseAdmin</span><span class="p">.</span><span class="nf">init</span><span class="p">()</span>
<span class="c1">// configure rest of project</span>
<span class="p">}.</span><span class="nf">start</span><span class="p">(</span><span class="n">wait</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now the Firebase Admin SDK is ready to use and we will learn to configure a Ktor authentication plugin to work with Firebase Authentication.</p>
<h2 id="setup-firebase-authentication">Setup Firebase Authentication</h2>
<p>With the Firebase Admin SDK initialized, it is time to create a <a href="https://ktor.io/docs/authentication.html">Ktor Authentication Provider</a> that can verify the JSON web token(JWT) from incoming requests are from an authenticated Firebase user.</p>
<h3 id="create-a-principal">Create a Principal</h3>
<p>First create a simple data class called <code class="language-plaintext highlighter-rouge">User</code> which will have some basic properties to represent a Firebase user, note how this extends the <code class="language-plaintext highlighter-rouge">Principal</code> interface to indicate to Ktor this class represents an authenticated principal. Feel free to add more properties to this file that fit your needs of what represents a user in your application.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">User</span><span class="p">(</span><span class="kd">val</span> <span class="py">userId</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span> <span class="kd">val</span> <span class="py">displayName</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Principal</span>
</code></pre></div></div>
<h3 id="create-authenticationprovider">Create AuthenticationProvider</h3>
<p>Now a Ktor <code class="language-plaintext highlighter-rouge">AuthenticationProvider</code> can be created which will verify the incoming request’s JWT and set the principal on the request to the current <code class="language-plaintext highlighter-rouge">User</code> if they are unauthenticated. I will have to credit <a href="https://stackoverflow.com/a/72446067/7900721">Aleksei Tirman for the inspiration for this solution</a>, although I did make a couple small tweaks to improve the error messaging and will try to break it down.</p>
<p>First create a <code class="language-plaintext highlighter-rouge">FirebaseConfig</code> class that extends <code class="language-plaintext highlighter-rouge">AuthenticationProvider.Config</code> which will provide a lambda to convert a Ktor Request and verified <code class="language-plaintext highlighter-rouge">FirebaseToken</code> to the <code class="language-plaintext highlighter-rouge">User</code> class.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FirebaseConfig</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">?)</span> <span class="p">:</span> <span class="nc">AuthenticationProvider</span><span class="p">.</span><span class="nc">Config</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span>
<span class="k">internal</span> <span class="kd">var</span> <span class="py">authHeader</span><span class="p">:</span> <span class="p">(</span><span class="nc">ApplicationCall</span><span class="p">)</span> <span class="p">-></span> <span class="nc">HttpAuthHeader</span><span class="p">?</span> <span class="p">=</span>
<span class="p">{</span> <span class="n">call</span> <span class="p">-></span> <span class="n">call</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="nf">parseAuthorizationHeaderOrNull</span><span class="p">()</span> <span class="p">}</span>
<span class="kd">var</span> <span class="py">firebaseAuthenticationFunction</span><span class="p">:</span> <span class="nc">AuthenticationFunction</span><span class="p"><</span><span class="nc">FirebaseToken</span><span class="p">></span> <span class="p">=</span> <span class="p">{</span>
<span class="k">throw</span> <span class="nc">NotImplementedError</span><span class="p">(</span><span class="nc">FirebaseImplementationError</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">validate</span><span class="p">(</span><span class="n">validate</span><span class="p">:</span> <span class="k">suspend</span> <span class="nc">ApplicationCall</span><span class="p">.(</span><span class="nc">FirebaseToken</span><span class="p">)</span> <span class="p">-></span> <span class="nc">User</span><span class="p">?)</span> <span class="p">{</span>
<span class="n">firebaseAuthenticationFunction</span> <span class="p">=</span> <span class="n">validate</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nc">ApplicationRequest</span><span class="p">.</span><span class="nf">parseAuthorizationHeaderOrNull</span><span class="p">():</span> <span class="nc">HttpAuthHeader</span><span class="p">?</span> <span class="p">=</span> <span class="k">try</span> <span class="p">{</span>
<span class="nf">parseAuthorizationHeader</span><span class="p">()</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">ex</span><span class="p">:</span> <span class="nc">IllegalArgumentException</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"failed to parse token"</span><span class="p">)</span>
<span class="k">null</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">const</span> <span class="kd">val</span> <span class="py">FirebaseImplementationError</span> <span class="p">=</span>
<span class="s">"Firebase auth validate function is not specified, use firebase { validate { ... } } to fix this"</span>
</code></pre></div></div>
<p>Now create the <code class="language-plaintext highlighter-rouge">FirebaseAuthProvider</code> class and extend the <code class="language-plaintext highlighter-rouge">AuthenticationProvider</code>. Here is where the bulk of the logic doing the verification with the Firebase Authentication will happen and set the <code class="language-plaintext highlighter-rouge">User</code> as the principal if the user request is authenticated.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">class</span> <span class="nc">FirebaseAuthProvider</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="nc">FirebaseConfig</span><span class="p">)</span> <span class="p">:</span> <span class="nc">AuthenticationProvider</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">authHeader</span><span class="p">:</span> <span class="p">(</span><span class="nc">ApplicationCall</span><span class="p">)</span> <span class="p">-></span> <span class="nc">HttpAuthHeader</span><span class="p">?</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="n">authHeader</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">authFunction</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="n">firebaseAuthenticationFunction</span>
<span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">onAuthenticate</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">AuthenticationContext</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">token</span> <span class="p">=</span> <span class="nf">authHeader</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">call</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">token</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
<span class="n">context</span><span class="p">.</span><span class="nf">challenge</span><span class="p">(</span>
<span class="nc">FirebaseJWTAuthKey</span><span class="p">,</span>
<span class="nc">AuthenticationFailedCause</span><span class="p">.</span><span class="nc">InvalidCredentials</span>
<span class="p">)</span> <span class="p">{</span> <span class="n">challengeFunc</span><span class="p">,</span> <span class="n">call</span> <span class="p">-></span>
<span class="n">challengeFunc</span><span class="p">.</span><span class="nf">complete</span><span class="p">()</span>
<span class="n">call</span><span class="p">.</span><span class="nf">respond</span><span class="p">(</span><span class="nc">UnauthorizedResponse</span><span class="p">(</span><span class="nc">HttpAuthHeader</span><span class="p">.</span><span class="nf">bearerAuthChallenge</span><span class="p">(</span><span class="n">realm</span> <span class="p">=</span> <span class="nc">FIREBASE_AUTH</span><span class="p">)))</span>
<span class="p">}</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">principal</span> <span class="p">=</span> <span class="nf">verifyFirebaseIdToken</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">call</span><span class="p">,</span> <span class="n">token</span><span class="p">,</span> <span class="n">authFunction</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">principal</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
<span class="n">context</span><span class="p">.</span><span class="nf">principal</span><span class="p">(</span><span class="n">principal</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">cause</span><span class="p">:</span> <span class="nc">Throwable</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">message</span> <span class="p">=</span> <span class="n">cause</span><span class="p">.</span><span class="n">message</span> <span class="o">?:</span> <span class="n">cause</span><span class="p">.</span><span class="n">javaClass</span><span class="p">.</span><span class="n">simpleName</span>
<span class="n">context</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nc">FirebaseJWTAuthKey</span><span class="p">,</span> <span class="nc">AuthenticationFailedCause</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">message</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">verifyFirebaseIdToken</span><span class="p">(</span>
<span class="n">call</span><span class="p">:</span> <span class="nc">ApplicationCall</span><span class="p">,</span>
<span class="n">authHeader</span><span class="p">:</span> <span class="nc">HttpAuthHeader</span><span class="p">,</span>
<span class="n">tokenData</span><span class="p">:</span> <span class="k">suspend</span> <span class="nc">ApplicationCall</span><span class="p">.(</span><span class="nc">FirebaseToken</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Principal</span><span class="p">?</span>
<span class="p">):</span> <span class="nc">Principal</span><span class="p">?</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">token</span><span class="p">:</span> <span class="nc">FirebaseToken</span> <span class="p">=</span> <span class="k">try</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">authHeader</span><span class="p">.</span><span class="n">authScheme</span> <span class="p">==</span> <span class="s">"Bearer"</span> <span class="p">&&</span> <span class="n">authHeader</span> <span class="k">is</span> <span class="nc">HttpAuthHeader</span><span class="p">.</span><span class="nc">Single</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">withContext</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">FirebaseAuth</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">().</span><span class="nf">verifyIdToken</span><span class="p">(</span><span class="n">authHeader</span><span class="p">.</span><span class="n">blob</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">null</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">ex</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
<span class="n">ex</span><span class="p">.</span><span class="nf">printStackTrace</span><span class="p">()</span>
<span class="k">return</span> <span class="k">null</span>
<span class="p">}</span> <span class="o">?:</span> <span class="k">return</span> <span class="k">null</span>
<span class="k">return</span> <span class="nf">tokenData</span><span class="p">(</span><span class="n">call</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nc">HttpAuthHeader</span><span class="p">.</span><span class="nc">Companion</span><span class="p">.</span><span class="nf">bearerAuthChallenge</span><span class="p">(</span><span class="n">realm</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">HttpAuthHeader</span> <span class="p">=</span>
<span class="nc">HttpAuthHeader</span><span class="p">.</span><span class="nc">Parameterized</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="nf">mapOf</span><span class="p">(</span><span class="nc">HttpAuthHeader</span><span class="p">.</span><span class="nc">Parameters</span><span class="p">.</span><span class="nc">Realm</span> <span class="n">to</span> <span class="n">realm</span><span class="p">))</span>
<span class="k">const</span> <span class="kd">val</span> <span class="py">FIREBASE_AUTH</span> <span class="p">=</span> <span class="s">"FIREBASE_AUTH"</span>
<span class="k">const</span> <span class="kd">val</span> <span class="py">FirebaseJWTAuthKey</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">"FirebaseAuth"</span>
</code></pre></div></div>
<p>Finally create an extension function on <code class="language-plaintext highlighter-rouge">AuthenticationConfig</code> which will create an instance of the <code class="language-plaintext highlighter-rouge">FirebaseAuthProvider</code> and register it to the Ktor application.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">AuthenticationConfig</span><span class="p">.</span><span class="nf">firebase</span><span class="p">(</span>
<span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="nc">FIREBASE_AUTH</span><span class="p">,</span>
<span class="n">configure</span><span class="p">:</span> <span class="nc">FirebaseConfig</span><span class="p">.()</span> <span class="p">-></span> <span class="nc">Unit</span>
<span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">provider</span> <span class="p">=</span> <span class="nc">FirebaseAuthProvider</span><span class="p">(</span><span class="nc">FirebaseConfig</span><span class="p">(</span><span class="n">name</span><span class="p">).</span><span class="nf">apply</span><span class="p">(</span><span class="n">configure</span><span class="p">))</span>
<span class="nf">register</span><span class="p">(</span><span class="n">provider</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="install-authentication-plugin">Install Authentication Plugin</h3>
<p>The <code class="language-plaintext highlighter-rouge">firebase()</code> extension function can now be used when installing the <code class="language-plaintext highlighter-rouge">Authentication</code> plugin on the Ktor <code class="language-plaintext highlighter-rouge">Application</code>. The <code class="language-plaintext highlighter-rouge">validate {}</code> lambda is where any additional information of a user could be looked up that does not exist on a <code class="language-plaintext highlighter-rouge">FirebaseToken</code> object.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Application</span><span class="p">.</span><span class="nf">configureFirebaseAuth</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">install</span><span class="p">(</span><span class="nc">Authentication</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">firebase</span> <span class="p">{</span>
<span class="nf">validate</span> <span class="p">{</span>
<span class="c1">// TODO look up user profile from DB</span>
<span class="nc">User</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">uid</span><span class="p">,</span> <span class="n">it</span><span class="p">.</span><span class="n">name</span><span class="p">.</span><span class="nf">orEmpty</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>Now call this extenstion function after the <code class="language-plaintext highlighter-rouge">FirebaseAdmin.init()</code> function to complete the integration.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">embeddedServer</span><span class="p">(</span><span class="nc">Netty</span><span class="p">,</span> <span class="n">port</span> <span class="p">=</span> <span class="mi">8080</span><span class="p">,</span> <span class="n">host</span> <span class="p">=</span> <span class="s">"0.0.0.0"</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">FirebaseAdmin</span><span class="p">.</span><span class="nf">init</span><span class="p">()</span>
<span class="nf">configureFirebaseAuth</span><span class="p">()</span>
<span class="p">}.</span><span class="nf">start</span><span class="p">(</span><span class="n">wait</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="create-authenticated-route">Create Authenticated Route</h3>
<p>Now with Firebase Authentication configured in the Ktor project, authenticated routes can be made using the same <code class="language-plaintext highlighter-rouge">FIREBASE_AUTH</code> constant that was used to register the plugin. Simply wrap any <code class="language-plaintext highlighter-rouge">Route</code> with <code class="language-plaintext highlighter-rouge">authenticate(FIREBASE_AUTH) { }</code>. If the user’s request has an invalid/expired JWT in the original request, the route should respond with an unauthorized 401 http status.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Route</span><span class="p">.</span><span class="nf">authenticatedRoute</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">authenticate</span><span class="p">(</span><span class="nc">FIREBASE_AUTH</span><span class="p">)</span> <span class="p">{</span>
<span class="k">get</span><span class="p">(</span><span class="s">"/authenticated"</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">user</span><span class="p">:</span> <span class="nc">User</span> <span class="p">=</span>
<span class="n">call</span><span class="p">.</span><span class="nf">principal</span><span class="p">()</span> <span class="o">?:</span> <span class="k">return</span><span class="nd">@get</span> <span class="n">call</span><span class="p">.</span><span class="nf">respond</span><span class="p">(</span><span class="nc">HttpStatusCode</span><span class="p">.</span><span class="nc">Unauthorized</span><span class="p">)</span>
<span class="n">call</span><span class="p">.</span><span class="nf">respond</span><span class="p">(</span><span class="s">"User is authenticated: $user"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="testing">Testing</h2>
<h3 id="manual-testing">Manual Testing</h3>
<p>To manually test the Firebase integration, you will need to get a valid JWT to send to the server in the authorization header. You may retrieve one the <a href="https://firebase.google.com/docs/reference/rest/auth#section-create-email-password">sign up</a> or <a href="https://firebase.google.com/docs/reference/rest/auth#section-sign-in-email-password">sign in</a> Firebase restful API. The example curl request below will make the sign up request, replace <code class="language-plaintext highlighter-rouge">insert-api-key</code> with your Firebase web api key which can be found in the Firebase Console under Project Settings.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--location</span> <span class="nt">--request</span> POST <span class="s1">'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=insert-api-key'</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-raw</span> <span class="s1">'{
"email" : "test@plusmobileapps.com",
"password" : "Password123!",
"returnSecureToken" : true
}'</span>
</code></pre></div></div>
<p>This should return a JSON object and you will need the <code class="language-plaintext highlighter-rouge">idToken</code> property for authenticated requests later.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"idToken"</span><span class="p">:</span><span class="w"> </span><span class="s2">"extract this token value"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Now you can make the request to your server injecting the token from the last step as the bearer for authentication.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--location</span> <span class="nt">--request</span> GET <span class="s1">'http://0.0.0.0:8080/authenticated'</span> <span class="se">\</span>
<span class="nt">--header</span> <span class="s1">'Authorization: Bearer insert-token-value'</span>
<span class="s2">"User is authenticated: User(userId=some-user-id, displayName=Andrew)"</span>
</code></pre></div></div>
<h3 id="unit-testing-authenticated-routes">Unit Testing Authenticated Routes</h3>
<p>To write unit tests for an authenticated route, we will create a <code class="language-plaintext highlighter-rouge">FirebaseAuthTestProvider</code> which will allow a mocked <code class="language-plaintext highlighter-rouge">User</code> to be provided and set as the principal.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FirebaseAuthTestProvider</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="nc">FirebaseTestConfig</span><span class="p">)</span> <span class="p">:</span> <span class="nc">AuthenticationProvider</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">authFunction</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">User</span><span class="p">?</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="n">mockAuthProvider</span>
<span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">onAuthenticate</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">AuthenticationContext</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">mockUser</span><span class="p">:</span> <span class="nc">User</span><span class="p">?</span> <span class="p">=</span> <span class="nf">authFunction</span><span class="p">()</span>
<span class="k">if</span> <span class="p">(</span><span class="n">mockUser</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
<span class="n">context</span><span class="p">.</span><span class="nf">principal</span><span class="p">(</span><span class="n">mockUser</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">context</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span>
<span class="nc">FirebaseJWTAuthKey</span><span class="p">,</span>
<span class="nc">AuthenticationFailedCause</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="s">"User was mocked to be unauthenticated"</span><span class="p">)</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">FirebaseTestConfig</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">?)</span> <span class="p">:</span> <span class="nc">AuthenticationProvider</span><span class="p">.</span><span class="nc">Config</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">mockAuthProvider</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">User</span><span class="p">?</span> <span class="p">=</span> <span class="p">{</span> <span class="k">null</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then create an extension function on <code class="language-plaintext highlighter-rouge">ApplicationTestBuilder</code> that will install the authentication plugin and register the <code class="language-plaintext highlighter-rouge">FirebaseAuthTestProvider</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">defaultTestUser</span> <span class="p">=</span> <span class="nc">User</span><span class="p">(</span><span class="n">userId</span> <span class="p">=</span> <span class="s">"some-user-id"</span><span class="p">,</span> <span class="n">displayName</span> <span class="p">=</span> <span class="s">"Darth Vader"</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">ApplicationTestBuilder</span><span class="p">.</span><span class="nf">mockAuthentication</span><span class="p">(</span><span class="n">mockAuth</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">User</span><span class="p">?</span> <span class="p">=</span> <span class="p">{</span> <span class="n">defaultTestUser</span> <span class="p">})</span> <span class="p">{</span>
<span class="nf">install</span><span class="p">(</span><span class="nc">Authentication</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">provider</span> <span class="p">=</span> <span class="nc">FirebaseAuthTestProvider</span><span class="p">(</span><span class="nc">FirebaseTestConfig</span><span class="p">(</span><span class="nc">FIREBASE_AUTH</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
<span class="n">mockAuthProvider</span> <span class="p">=</span> <span class="n">mockAuth</span>
<span class="p">})</span>
<span class="nf">register</span><span class="p">(</span><span class="n">provider</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="create-a-ktor-test">Create a Ktor Test</h3>
<p>To write a <a href="https://ktor.io/docs/testing.html">Ktor test</a> for an authenticated route, make use of the newly created <code class="language-plaintext highlighter-rouge">mockAuthentication { }</code> function, install the authenticated route under test, and call it with the client.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AuthenticatedRouteTest</span> <span class="p">{</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`authenticated</span> <span class="n">route</span> <span class="p">-</span> <span class="k">is</span> <span class="nf">authenticated`</span><span class="p">()</span> <span class="p">=</span> <span class="nf">testApplication</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="nc">User</span><span class="p">(</span><span class="s">"some id"</span><span class="p">,</span> <span class="s">"Andrew"</span><span class="p">)</span>
<span class="nf">mockAuthentication</span> <span class="p">{</span> <span class="n">user</span> <span class="p">}</span>
<span class="nf">routing</span> <span class="p">{</span> <span class="nf">authenticatedRoute</span><span class="p">()</span> <span class="p">}</span>
<span class="n">client</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s">"/authenticated"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="nc">HttpStatusCode</span><span class="p">.</span><span class="nc">OK</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="s">"User is authenticated: $user"</span><span class="p">,</span> <span class="nf">bodyAsText</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Also worth mentioning since the <code class="language-plaintext highlighter-rouge">mockAuth</code> function parameter defaults to returning the <code class="language-plaintext highlighter-rouge">defaultTestUser</code>, this authenticated test could also be rewritten like so:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`authenticated</span> <span class="n">route</span> <span class="p">-</span> <span class="k">is</span> <span class="nf">authenticated`</span><span class="p">()</span> <span class="p">=</span> <span class="nf">testApplication</span> <span class="p">{</span>
<span class="nf">mockAuthentication</span><span class="p">()</span>
<span class="nf">routing</span> <span class="p">{</span> <span class="nf">authenticatedRoute</span><span class="p">()</span> <span class="p">}</span>
<span class="n">client</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s">"/authenticated"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="nc">HttpStatusCode</span><span class="p">.</span><span class="nc">OK</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="s">"User is authenticated: $defaultTestUser"</span><span class="p">,</span> <span class="nf">bodyAsText</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you were so inclined to test an unauthorized user, simply return null in the <code class="language-plaintext highlighter-rouge">mockAuthentication { }</code> lambda.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`authenticated</span> <span class="n">route</span> <span class="p">-</span> <span class="k">is</span> <span class="nf">unauthorized`</span><span class="p">()</span> <span class="p">=</span> <span class="nf">testApplication</span> <span class="p">{</span>
<span class="nf">mockAuthentication</span> <span class="p">{</span> <span class="k">null</span> <span class="p">}</span>
<span class="nf">routing</span> <span class="p">{</span> <span class="nf">authenticatedRoute</span><span class="p">()</span> <span class="p">}</span>
<span class="n">client</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s">"/authenticated"</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="nc">HttpStatusCode</span><span class="p">.</span><span class="nc">Unauthorized</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>At this point, you should have a Ktor server configured with Firebase authentication and learned how to write Ktor tests with a Firebase test authentication provider. If you wish to see all the source code for this project please check out the link below. Happy coding!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/plusmobileapps/ktor-firebase-auth-sample">Github Repository - Source Code</a></li>
</ul>Andrew Steinmetzandrew@plusmobileappsWith the release of Ktor 2.0, one of the migrations I had to do was for Firebase Authentication which I first learned about how to use with Ktor 1.6 from this medium article last year. Learn how to setup Firebase Authentication with Ktor 2.0 and how to test it.How to track view impressions in a Jetpack Compose Lazy Column2022-05-04T00:00:00+00:002022-05-04T00:00:00+00:00https://plusmobileapps.com/2022/05/04/lazy-column-view-impressions<p>If you have ever shipped a feature with a scrolling list, a product manager will usually ask you to track when an item in that list is viewed by the user. With Jetpack Compose being somewhat new, I was curious how to solve this problem with respect to a <code class="language-plaintext highlighter-rouge">LazyColumn</code> so let’s learn how to know the second eyeballs see items as they scroll into view!</p>
<p><img src="/assets/images/spongebob-eyes.gif" alt="" /></p>
<!--more-->
<h2 id="build-the-ui">Build the UI</h2>
<p>The data model for the list will be a very simple data class that has a <code class="language-plaintext highlighter-rouge">key</code> property which will be important for use with the <code class="language-plaintext highlighter-rouge">LazyColumn</code> to know exactly which items are coming into view. The <code class="language-plaintext highlighter-rouge">key</code> can technically be <code class="language-plaintext highlighter-rouge">Any</code> type, the important thing is to ensure there is an <code class="language-plaintext highlighter-rouge">equals</code> method on whatever type you choose so for simplicity in this example we will make it a <code class="language-plaintext highlighter-rouge">String</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">Person</span><span class="p">(</span><span class="kd">val</span> <span class="py">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
</code></pre></div></div>
<p>To start off building the UI for this sample, we will start with a <code class="language-plaintext highlighter-rouge">LazyColumn</code> lifting the <code class="language-plaintext highlighter-rouge">LazyListState</code> up as this will become important later to calculate exactly which items are scrolling into view. The other important callout here is declaring the key for the items in the <code class="language-plaintext highlighter-rouge">LazyColumn</code> by passing in the <code class="language-plaintext highlighter-rouge">key</code> property on a <code class="language-plaintext highlighter-rouge">Person</code> discussed earlier. Finally, pass the lazy list state down into each item in the list.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">ListView</span><span class="p">(</span>
<span class="n">people</span><span class="p">:</span> <span class="nc">List</span><span class="p"><</span><span class="nc">Person</span><span class="p">>,</span>
<span class="n">onDeleteClicked</span><span class="p">:</span> <span class="p">(</span><span class="nc">Person</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">,</span>
<span class="n">onItemViewed</span><span class="p">:</span> <span class="p">(</span><span class="nc">Person</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Unit</span>
<span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">lazyListState</span> <span class="p">=</span> <span class="nf">rememberLazyListState</span><span class="p">()</span> <span class="c1">// lift the lazy list state</span>
<span class="nc">LazyColumn</span><span class="p">(</span><span class="n">state</span> <span class="p">=</span> <span class="n">lazyListState</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">items</span><span class="p">(</span><span class="n">people</span><span class="p">.</span><span class="n">size</span><span class="p">,</span> <span class="n">key</span> <span class="p">=</span> <span class="p">{</span> <span class="n">people</span><span class="p">[</span><span class="n">it</span><span class="p">].</span><span class="n">key</span> <span class="p">})</span> <span class="p">{</span> <span class="c1">// declare the key for item</span>
<span class="kd">val</span> <span class="py">person</span> <span class="p">=</span> <span class="n">people</span><span class="p">[</span><span class="n">it</span><span class="p">]</span>
<span class="nc">PersonRow</span><span class="p">(</span><span class="n">lazyListState</span><span class="p">,</span> <span class="n">person</span><span class="p">,</span> <span class="n">onDeleteClicked</span><span class="p">,</span> <span class="n">onItemViewed</span><span class="p">)</span> <span class="c1">// pass lazy list state into item</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now looking at the <code class="language-plaintext highlighter-rouge">PersonRow</code> composable, we will make use of an <code class="language-plaintext highlighter-rouge">ItemImpression</code> composable passing in the lazy list state as this will be where the logic for knowing when this item was scrolled into view.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">PersonRow</span><span class="p">(</span><span class="n">lazyListState</span><span class="p">:</span> <span class="nc">LazyListState</span><span class="p">,</span> <span class="n">person</span><span class="p">:</span> <span class="nc">Person</span><span class="p">,</span> <span class="n">onDeleteClicked</span><span class="p">:</span> <span class="p">(</span><span class="nc">Person</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">,</span> <span class="n">onItemViewed</span><span class="p">:</span> <span class="p">(</span><span class="nc">Person</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">ItemImpression</span><span class="p">(</span><span class="n">key</span> <span class="p">=</span> <span class="n">person</span><span class="p">.</span><span class="n">key</span><span class="p">,</span> <span class="n">lazyListState</span> <span class="p">=</span> <span class="n">lazyListState</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">onItemViewed</span><span class="p">(</span><span class="n">person</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// omitted UI code for row </span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now the <code class="language-plaintext highlighter-rouge">ItemImpression</code> composable technically doesn’t have any UI related code in it as its really just concerned with determining when a specific <code class="language-plaintext highlighter-rouge">key</code> has scrolled into view of the <code class="language-plaintext highlighter-rouge">LazyListState</code>. However, we will make use of <a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#derivedStateOf(kotlin.Function0)"><code class="language-plaintext highlighter-rouge">derivedStateOf</code></a> in Compose to ensure that the <code class="language-plaintext highlighter-rouge">isItemWithKeyInView</code> is calculated when the state of the <code class="language-plaintext highlighter-rouge">lazyListState</code> changes, but will only cause recomposition when the value of the derived state changes. Then the <a href="https://developer.android.com/jetpack/compose/side-effects#launchedeffect"><code class="language-plaintext highlighter-rouge">LaunchedEffect</code></a> will fire exactly one time since <code class="language-plaintext highlighter-rouge">Unit</code> is being passed in as the key which notifies when the item was viewed.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">ItemImpression</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">Any</span><span class="p">,</span> <span class="n">lazyListState</span><span class="p">:</span> <span class="nc">LazyListState</span><span class="p">,</span> <span class="n">onItemViewed</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">isItemWithKeyInView</span> <span class="k">by</span> <span class="nf">remember</span> <span class="p">{</span>
<span class="nf">derivedStateOf</span> <span class="p">{</span>
<span class="n">lazyListState</span><span class="p">.</span><span class="n">layoutInfo</span>
<span class="p">.</span><span class="n">visibleItemsInfo</span>
<span class="p">.</span><span class="nf">any</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">key</span> <span class="p">==</span> <span class="n">key</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">isItemWithKeyInView</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">LaunchedEffect</span><span class="p">(</span><span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span> <span class="nf">onItemViewed</span><span class="p">()</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="analytics-tracker">Analytics Tracker</h2>
<p>With the compose code written so far, this will notify when an item is scrolled into view. However, it will notify when an item is not just scrolled from the bottom into view but also being scrolled back into view from the top. Most product managers probably only care to know that an item was viewed once, which is pretty easy to ensure by making use of a <code class="language-plaintext highlighter-rouge">HashSet</code> and checking if that key exists in the <code class="language-plaintext highlighter-rouge">HashSet</code> before determining if the impression analytics event should be fired.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AnalyticsTracker</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">recordedPeople</span> <span class="p">=</span> <span class="n">hashSetOf</span><span class="p"><</span><span class="nc">String</span><span class="p">>()</span>
<span class="k">fun</span> <span class="nf">onPersonViewed</span><span class="p">(</span><span class="n">person</span><span class="p">:</span> <span class="nc">Person</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">recordedPeople</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">person</span><span class="p">.</span><span class="n">key</span><span class="p">))</span> <span class="k">return</span>
<span class="n">recordedPeople</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">person</span><span class="p">.</span><span class="n">key</span><span class="p">)</span>
<span class="nc">Log</span><span class="p">.</span><span class="nf">d</span><span class="p">(</span><span class="s">"Item Impression"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="viewmodel">ViewModel</h2>
<p>One of the last things to glue everything together is making a <code class="language-plaintext highlighter-rouge">ViewModel</code> to delegate events to the tracker for view impressions of items and manage the state of the list. Note for simplicity the tracker is just instantiated in the <code class="language-plaintext highlighter-rouge">ViewModel</code>, in a production project one would inject this dependency with your dependency injection framework of choice. I didn’t want to over complicate this sample with a DI framework though.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MainViewModel</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">tracker</span> <span class="p">=</span> <span class="nc">AnalyticsTracker</span><span class="p">()</span>
<span class="k">private</span> <span class="kd">var</span> <span class="py">_state</span><span class="p">:</span> <span class="nc">MutableStateFlow</span><span class="p"><</span><span class="nc">List</span><span class="p"><</span><span class="nc">Person</span><span class="p">>></span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p">(</span><span class="n">people</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">state</span><span class="p">:</span> <span class="nc">StateFlow</span><span class="p"><</span><span class="nc">List</span><span class="p"><</span><span class="nc">Person</span><span class="p">>></span> <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="n">_state</span>
<span class="k">fun</span> <span class="nf">onDeleteClicked</span><span class="p">(</span><span class="n">person</span><span class="p">:</span> <span class="nc">Person</span><span class="p">)</span> <span class="p">{</span>
<span class="n">_state</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">_state</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="nf">toMutableList</span><span class="p">().</span><span class="nf">also</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">person</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">onPersonViewed</span><span class="p">(</span><span class="n">person</span><span class="p">:</span> <span class="nc">Person</span><span class="p">)</span> <span class="p">{</span>
<span class="n">tracker</span><span class="p">.</span><span class="nf">onPersonViewed</span><span class="p">(</span><span class="n">person</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="final-outcome">Final Outcome</h2>
<p><img src="/assets/images/lazy-column-view.gif" alt="" /></p>
<p>That’s it! If you want to look at complete source code for this sample, it is linked in the section below. Hope this helps and don’t be afraid to leave a comment!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/plusmobileapps/lazycolumn-view-impressions">Github Repository</a></li>
<li><a href="https://stackoverflow.com/a/70951303/7900721">Stack Overflow Answer</a> - an efficient way to check when a specific LazyColumn item comes into view</li>
</ul>Andrew Steinmetzandrew@plusmobileappsIf you have ever shipped a feature with a scrolling list, a product manager will usually ask you to track when an item in that list is viewed by the user. With Jetpack Compose being somewhat new, I was curious how to solve this problem with respect to a LazyColumn so let’s learn how to know the second eyeballs see items as they scroll into view!Surviving Android Process Death With SavedStateFlow2022-01-06T00:00:00+00:002022-01-06T00:00:00+00:00https://plusmobileapps.com/2022/01/06/save-state-flow<p>I was perusing Reddit the other day when someone <a href="https://www.reddit.com/r/androiddev/comments/rlxrsr/in_stateflow_how_can_we_save_and_restore_android/">asked</a> how they could use <a href="https://developer.android.com/reference/androidx/lifecycle/SavedStateHandle"><code class="language-plaintext highlighter-rouge">SavedStateHandle</code></a> with a <a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/"><code class="language-plaintext highlighter-rouge">StateFlow</code></a> similar to the <a href="https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate"><code class="language-plaintext highlighter-rouge">SavedStateHandle.getLiveData()</code></a> version. The most upvoted comment originally was saying that this functionality is not officially supported, but one could convert the <code class="language-plaintext highlighter-rouge">LiveData</code> to a <code class="language-plaintext highlighter-rouge">Flow</code> using the <code class="language-plaintext highlighter-rouge">LiveData.asFlow()</code> extension function. That seemed pretty simple for anyone to do, however testing that would then require using <code class="language-plaintext highlighter-rouge">LiveData</code> in your tests which might be annoying if you were using <code class="language-plaintext highlighter-rouge">StateFlow</code> to manage state. So after looking over the API, it seemed pretty simple to write a wrapper that could expose this functionality directly as a <code class="language-plaintext highlighter-rouge">StateFlow</code> and that is how the <a href="https://plusmobileapps.com/SavedStateFlow/">SavedStateFlow</a> library was made!</p>
<!--more-->
<h2 id="savedstateflow-api">SavedStateFlow API</h2>
<p>At its core, the API for <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> is very simple as it’s supposed to be similar to a <code class="language-plaintext highlighter-rouge">MutableStateFlow</code>. There is a <code class="language-plaintext highlighter-rouge">value</code> property that can be mutated and a method that can expose it as a <code class="language-plaintext highlighter-rouge">StateFlow</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">SavedStateFlow</span><span class="p"><</span><span class="nc">T</span><span class="p">></span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">value</span><span class="p">:</span> <span class="nc">T</span>
<span class="k">fun</span> <span class="nf">asStateFlow</span><span class="p">():</span> <span class="nc">StateFlow</span><span class="p"><</span><span class="nc">T</span><span class="p">></span>
<span class="p">}</span>
</code></pre></div></div>
<p>The implementation detail of this interface will simply delegate value changes to the <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> and will observe any value changes from the <code class="language-plaintext highlighter-rouge">SavedStateHandle.getLiveData()</code> function. Then the initial value for the <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> will first be retrieved by the <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> and if one does not exist then it will default to the one provided by yourself.</p>
<h2 id="how-to-create-a-savedstateflow">How to create a SavedStateFlow?</h2>
<p><code class="language-plaintext highlighter-rouge">SavedStateFlow</code> is just an interface, so how does one create an instance of one? Well since <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> can’t create this, there is a new wrapper called <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code>. The library includes an extension function on <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> to create a reference to a <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">savedStateHandle</span><span class="p">:</span> <span class="nc">SavedStateHandle</span> <span class="p">=</span> <span class="nc">TODO</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">savedStateFlowHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span> <span class="p">=</span>
<span class="n">savedStateHandle</span><span class="p">.</span><span class="nf">toSavedStateFlowHandle</span><span class="p">()</span>
</code></pre></div></div>
<p>Now this new <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code> provides two new functions on top of the original <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> API.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">SavedStateFlowHandle</span> <span class="p">{</span>
<span class="nd">@MainThread</span>
<span class="k">fun</span> <span class="p"><</span><span class="nc">T</span><span class="p">></span> <span class="nf">getSavedStateFlow</span><span class="p">(</span>
<span class="n">viewModelScope</span><span class="p">:</span> <span class="nc">CoroutineScope</span><span class="p">,</span>
<span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
<span class="n">defaultValue</span><span class="p">:</span> <span class="nc">T</span>
<span class="p">):</span> <span class="nc">SavedStateFlow</span><span class="p"><</span><span class="nc">T</span><span class="p">></span>
<span class="nd">@MainThread</span>
<span class="k">fun</span> <span class="p"><</span><span class="nc">T</span><span class="p">></span> <span class="nf">getFlow</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">T</span><span class="p">></span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">getFlow()</code> function is pretty self explanatory and exposes the <code class="language-plaintext highlighter-rouge">SavedStateHandle.getLiveData()</code> as a <code class="language-plaintext highlighter-rouge">Flow</code> directly, which could help for unit testing avoiding the need to mess around with <code class="language-plaintext highlighter-rouge">LiveData</code> directly.</p>
<p>The <code class="language-plaintext highlighter-rouge">getSavedStateFlow()</code> is the real meat and potatoes of this library as that is how to create an instance of a <code class="language-plaintext highlighter-rouge">SavedStateFlow</code>. Notice the first parameter to this function is <code class="language-plaintext highlighter-rouge">viewModelScope</code>, that is because the <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> will use that <code class="language-plaintext highlighter-rouge">CoroutineScope</code> to collect new values from the <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> whenever the value changes and will also stop collecting the values when the <code class="language-plaintext highlighter-rouge">ViewModel</code> itself is cleared. So putting everything together, one simple usage of <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> might look like the following:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MainViewModel</span><span class="p">(</span>
<span class="n">savedStateFlowHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span><span class="p">,</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">newsDataSource</span><span class="p">:</span> <span class="nc">NewsDataSource</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">query</span><span class="p">:</span> <span class="nc">SavedStateFlow</span><span class="p"><</span><span class="nc">String</span><span class="p">></span> <span class="p">=</span>
<span class="n">savedStateFlowHandle</span><span class="p">.</span><span class="nf">getSavedStateFlow</span><span class="p">(</span>
<span class="n">viewModelScope</span> <span class="p">=</span> <span class="n">viewModelScope</span><span class="p">,</span>
<span class="n">key</span> <span class="p">=</span> <span class="s">"main-viewmodel-query-key"</span><span class="p">,</span>
<span class="n">defaultValue</span> <span class="p">=</span> <span class="s">""</span>
<span class="p">)</span>
<span class="nf">init</span> <span class="p">{</span>
<span class="nf">observeQuery</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">updateQuery</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="n">query</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">query</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">fun</span> <span class="nf">observeQuery</span><span class="p">()</span> <span class="p">{</span>
<span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
<span class="n">query</span><span class="p">.</span><span class="nf">asStateFlow</span><span class="p">()</span>
<span class="p">.</span><span class="nf">flatMapLatest</span> <span class="p">{</span> <span class="n">query</span> <span class="p">-></span>
<span class="c1">// fetch the results for the latest query</span>
<span class="n">newsDataSource</span><span class="p">.</span><span class="nf">fetchQuery</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">results</span> <span class="p">-></span>
<span class="c1">// Update with the latest results</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p class="warning">Since <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> is a wrapper around <code class="language-plaintext highlighter-rouge">SavedStateHandle</code>, the following note from the <a href="https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate">documentation</a> should be observed. “State must be simple and lightweight. For complex or large data, you should use <a href="https://developer.android.com/topic/libraries/architecture/saving-states#local">local persistence</a>.”</p>
<h2 id="how-to-inject-savedstateflowhandle">How to inject SavedStateFlowHandle?</h2>
<p>In the sample above, there was an extension function on <code class="language-plaintext highlighter-rouge">SavedStateHandle</code> to get an instance of a <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code>. Some of you might be wondering how one actually injects that into a <code class="language-plaintext highlighter-rouge">ViewModel</code>. Well if you’re doing manual injection, this is pretty simple using the <code class="language-plaintext highlighter-rouge">AbstractSavedStateViewModelFactory</code> and there is a sample of this in the <a href="https://plusmobileapps.com/SavedStateFlow/manual-di/">documentation</a>.</p>
<p>However, where this library really shines in the developer experience is when you are using <a href="https://developer.android.com/training/dependency-injection/hilt-android">Hilt</a> because there is a separate <a href="https://plusmobileapps.com/SavedStateFlow/setup/#savedstateflow-hilt">artifact</a> which can automatically scope a <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code> to any <code class="language-plaintext highlighter-rouge">@HiltViewModel</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@HiltViewModel</span>
<span class="kd">class</span> <span class="nc">MainViewModel</span> <span class="nd">@Inject</span> <span class="k">constructor</span><span class="p">(</span>
<span class="n">savedStateFlowHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span>
</code></pre></div></div>
<p>However, not every <code class="language-plaintext highlighter-rouge">ViewModel</code> can be annotated with <code class="language-plaintext highlighter-rouge">@HiltViewModel</code> when values passed through the constructor are determined at runtime which is when assisted injection can be used. In this scenario, there are a few extension methods provided which can provide a <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code> when using assisted injection.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyAssistedViewModel</span> <span class="nd">@AssistedInject</span> <span class="k">constructor</span><span class="p">(</span>
<span class="nd">@Assisted</span> <span class="n">savedStateFlowHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span><span class="p">,</span>
<span class="nd">@Assisted</span> <span class="n">id</span><span class="p">:</span> <span class="nc">String</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="nd">@AssistedFactory</span>
<span class="kd">interface</span> <span class="nc">Factory</span> <span class="p">{</span>
<span class="k">fun</span> <span class="nf">create</span><span class="p">(</span><span class="n">savedStateFlowHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span><span class="p">,</span> <span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">MyAssistedViewModel</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nd">@AndroidEntryPoint</span>
<span class="kd">class</span> <span class="nc">AssistedFragment</span> <span class="p">:</span> <span class="nc">Fragment</span><span class="p">()</span> <span class="p">{</span>
<span class="nd">@Inject</span>
<span class="k">lateinit</span> <span class="kd">var</span> <span class="py">factory</span><span class="p">:</span> <span class="nc">MyAssistedViewModel</span><span class="p">.</span><span class="nc">Factory</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">viewModel</span><span class="p">:</span> <span class="nc">MyAssistedViewModel</span> <span class="k">by</span> <span class="nf">assistedViewModel</span> <span class="p">{</span> <span class="n">savedStateFlowHandle</span> <span class="p">-></span>
<span class="n">factory</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">savedStateFlowHandle</span><span class="p">,</span> <span class="n">arguments</span><span class="o">?.</span><span class="nf">getString</span><span class="p">(</span><span class="s">"some-argument-key"</span><span class="p">)</span><span class="o">!!</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For more information on the <code class="language-plaintext highlighter-rouge">SavedStateFlow</code> Hilt integration, please check out the <a href="https://plusmobileapps.com/SavedStateFlow/hilt-di/">documentation</a>.</p>
<h2 id="testing">Testing</h2>
<p>The main motivation for writing this library was for testing and to avoid messing around with <code class="language-plaintext highlighter-rouge">LiveData</code>, so there is a test artifact that can be used for unit tests called <code class="language-plaintext highlighter-rouge">TestSavedStateFlow</code>. The addition to this class allows you to provide a default value or a cached value which is null by default for different testing scenarios. One basic usage of this artifact with <a href="https://mockk.io/">Mockk</a> is as shown below:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SomeTest</span> <span class="p">{</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`some</span> <span class="nf">test`</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">savedStateHandle</span><span class="p">:</span> <span class="nc">SavedStateFlowHandle</span> <span class="p">=</span> <span class="nf">mockk</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">savedStateFlow</span> <span class="p">=</span> <span class="nc">TestSavedStateFlow</span><span class="p"><</span><span class="nc">String</span><span class="p">>(</span>
<span class="n">defaultValue</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
<span class="n">cachedValue</span> <span class="p">=</span> <span class="s">"some cached value"</span>
<span class="p">)</span>
<span class="nf">every</span> <span class="p">{</span> <span class="n">savedStateHandle</span><span class="p">.</span><span class="nf">getSavedStateFlow</span><span class="p">(</span><span class="nf">any</span><span class="p">(),</span> <span class="s">"some-key"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span> <span class="p">}</span> <span class="n">returns</span> <span class="n">savedStateFlow</span>
<span class="kd">val</span> <span class="py">viewModel</span> <span class="p">=</span> <span class="nc">MyViewModel</span><span class="p">(</span><span class="n">savedStateHandle</span><span class="p">)</span>
<span class="c1">// omitted test code</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For more information on testing and how this could be used with <a href="https://github.com/cashapp/turbine">Turbine</a>, please check out the <a href="https://plusmobileapps.com/SavedStateFlow/testing/">documentation</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this article we went over how to create/use a <code class="language-plaintext highlighter-rouge">SavedStateFlow</code>, inject a <code class="language-plaintext highlighter-rouge">SavedStateFlowHandle</code> into a <code class="language-plaintext highlighter-rouge">ViewModel</code> and how to test with <code class="language-plaintext highlighter-rouge">TestSavedStateFlow</code>. I highly encourage you to check out the documentation which has more detailed samples and the GitHub repository if you want to take a look at the source code or even make a contribution if you see ways it could be improved.</p>
<p>Hope someone else finds this library useful until Google decides to support this functionality officially sometime in the future. Enjoy!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/plusmobileapps/SavedStateFlow">Github Repository</a></li>
<li><a href="https://plusmobileapps.com/SavedStateFlow/">Project site</a> - documentation</li>
<li><a href="https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/">Publishing Android libraries to MavenCentral in 2021</a> - I have never published a library before and this article was very helpful for getting this library up on MavenCentral</li>
</ul>Andrew Steinmetzandrew@plusmobileappsI was perusing Reddit the other day when someone asked how they could use SavedStateHandle with a StateFlow similar to the SavedStateHandle.getLiveData() version. The most upvoted comment originally was saying that this functionality is not officially supported, but one could convert the LiveData to a Flow using the LiveData.asFlow() extension function. That seemed pretty simple for anyone to do, however testing that would then require using LiveData in your tests which might be annoying if you were using StateFlow to manage state. So after looking over the API, it seemed pretty simple to write a wrapper that could expose this functionality directly as a StateFlow and that is how the SavedStateFlow library was made!How to test a custom Android view with Robolectric2020-12-14T00:00:00+00:002020-12-14T00:00:00+00:00https://plusmobileapps.com/2020/12/14/android-custom-view-testing<p>Working at an enterprise that needs a custom component library with each component being its own snow flake and having more complicated logic than the next. I found myself needing a way to test the logic in these views to ensure I could iterate quickly and not break anything in the process.</p>
<!--more-->
<p>Having a lot of prior experience unit testing business logic at the presentation and domain layer, it was always best practice to remove all Android references as this can be very tricky to mock. So testing the logic in an Android view was somewhat foreign to me, until I came across a <a href="https://proandroiddev.com/testing-custom-views-with-robolectric-4-bac7226f4a52">medium article</a> that had a very simple example of how this could be achieved with <a href="http://robolectric.org">Robolectric</a> by asserting the text in <code class="language-plaintext highlighter-rouge">TextView</code>. So in this post I will try to expand on this concept with a slightly more complex example and give some tips for utilizing Kotlin to write more idiomatic tests.</p>
<p class="warning">At the time this post was written, <a href="https://developer.android.com/jetpack/compose">Jetpack Compose</a> was only in the alpha stage which provides a more robust solution for building and <a href="https://developer.android.com/jetpack/compose/testing">testing</a> custom components. Enterprises like stable things though, so that’s what this post will describe and encourage you to check out compose if it is stable or you want to be on the bleeding edge of technology.</p>
<h2 id="create-a-components-library">Create a Components Library</h2>
<p>If you don’t plan on splitting up your custom components into a separate module, then skip ahead to the next section. If you are wanting to achieve separation of concerns and encapsulate the dependencies of your custom views, then we will need to create a new library module.</p>
<p>You can create a new module under <code class="language-plaintext highlighter-rouge">File -> New -> New Module...</code> and select Android library. Give the module a name and select finish.</p>
<p><img src="/assets/images/create-new-android-library.png" alt="" /></p>
<p>Then in the <code class="language-plaintext highlighter-rouge">app/build.gradle</code>, you will need to add this module to the dependencies so that custom view can be accessed from the Android app module.</p>
<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">dependencies</span> <span class="o">{</span>
<span class="n">implementation</span> <span class="nf">project</span><span class="o">(</span><span class="s1">':plusmobileappsui'</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Everything else should have been auto generated when you created the library module and can move onto making the custom view. All the work for this section can be found in this <a href="https://github.com/plusmobileapps/test-custom-android-view/commit/68fd4075bae5de4c758d51aff2627c215f014802">commit</a>.</p>
<h2 id="create-a-custom-android-view">Create a Custom Android View</h2>
<p>The component that will be built is a material card which has a lock button, text description, and a background ripple that is only active when the card is unlocked.</p>
<p><img src="/assets/images/android_custom_view.gif" alt="" /></p>
<p>I am not going to go into great detail of how this custom view itself was built to focus more on the testing side of the view. The official Android documentation has a great tutorial for <a href="https://developer.android.com/training/custom-views/create-view">how to create a custom Android view</a> if you want to learn more about how to do that. Otherwise you can see all the code needed for creating this custom view in this <a href="https://github.com/plusmobileapps/test-custom-android-view/commit/0bce65404694984c247553b299912554306fdbf0">commit</a>. Only useful things to know for testing later is the name of the view, <code class="language-plaintext highlighter-rouge">MyCustomView</code>, and the xml attributes as we will be injecting them into the constructor of the Robolectric tests.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><resources></span>
<span class="nt"><declare-styleable</span> <span class="na">name=</span><span class="s">"MyCustomView"</span><span class="nt">></span>
<span class="nt"><attr</span> <span class="na">name=</span><span class="s">"isLocked"</span> <span class="na">format=</span><span class="s">"boolean"</span> <span class="nt">/></span>
<span class="nt"><attr</span> <span class="na">name=</span><span class="s">"unlockLabel"</span> <span class="na">format=</span><span class="s">"string"</span><span class="nt">/></span>
<span class="nt"><attr</span> <span class="na">name=</span><span class="s">"lockLabel"</span> <span class="na">format=</span><span class="s">"string"</span><span class="nt">/></span>
<span class="nt"></declare-styleable></span>
<span class="nt"></resources></span>
</code></pre></div></div>
<h2 id="writing-robolectric-tests">Writing Robolectric Tests</h2>
<h3 id="setup-robolectric-in-project">Setup Robolectric in Project</h3>
<p>In the module where your custom view is, add the following to the <code class="language-plaintext highlighter-rouge">build.gradle</code> in order to run Robolectric tests.</p>
<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">android</span> <span class="o">{</span>
<span class="n">testOptions</span> <span class="o">{</span>
<span class="n">unitTests</span> <span class="o">{</span>
<span class="n">includeAndroidResources</span> <span class="o">=</span> <span class="kc">true</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">dependencies</span> <span class="o">{</span>
<span class="n">testImplementation</span> <span class="s1">'org.robolectric:robolectric:4.4'</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="setup-robolectric-test">Setup Robolectric Test</h3>
<p>The quickest way to create a test is opening up <code class="language-plaintext highlighter-rouge">MyCustomView</code>, pressing <code class="language-plaintext highlighter-rouge">ctrl + enter</code> to bring up the <code class="language-plaintext highlighter-rouge">Generate</code> menu and select <code class="language-plaintext highlighter-rouge">Test..</code>.</p>
<p><img src="/assets/images/generate-test.png" alt="" /></p>
<p>Android studio should auto generate the name, click on finish and make sure to select the <code class="language-plaintext highlighter-rouge">test</code> folder and not the <code class="language-plaintext highlighter-rouge">androidTest</code> folder since Robolectric can run locally on your machine.</p>
<p><img src="/assets/images/generate-test-name.png" alt="" /></p>
<p>Now in our test class, we need to annotate our test with the Robolectric test runner and can create a setup function to instantiate the view with the context of an <code class="language-plaintext highlighter-rouge">Activity</code> from Robolectric.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RunWith</span><span class="p">(</span><span class="nc">RobolectricTestRunner</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="nd">@Config</span><span class="p">(</span><span class="n">sdk</span> <span class="p">=</span> <span class="p">[</span><span class="nc">Build</span><span class="p">.</span><span class="nc">VERSION_CODES</span><span class="p">.</span><span class="nc">O_MR1</span><span class="p">])</span> <span class="c1">//needed unless you run your tests with java 9</span>
<span class="kd">class</span> <span class="nc">MyCustomViewTest</span> <span class="p">{</span>
<span class="k">private</span> <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">myCustomView</span><span class="p">:</span> <span class="nc">MyCustomView</span>
<span class="k">private</span> <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">rootView</span><span class="p">:</span> <span class="nc">ConstraintLayout</span>
<span class="k">private</span> <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">lockButton</span><span class="p">:</span> <span class="nc">ImageButton</span>
<span class="k">private</span> <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">lockDescription</span><span class="p">:</span> <span class="nc">TextView</span>
<span class="nd">@Before</span>
<span class="k">fun</span> <span class="nf">setUp</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">activityController</span> <span class="p">=</span> <span class="nc">Robolectric</span><span class="p">.</span><span class="nf">buildActivity</span><span class="p">(</span><span class="nc">Activity</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">activity</span> <span class="p">=</span> <span class="n">activityController</span><span class="p">.</span><span class="k">get</span><span class="p">()</span>
<span class="n">myCustomView</span> <span class="p">=</span> <span class="nc">MyCustomView</span><span class="p">(</span><span class="n">activity</span><span class="p">,</span> <span class="n">attributeSet</span><span class="p">)</span>
<span class="n">rootView</span> <span class="p">=</span> <span class="n">myCustomView</span><span class="p">.</span><span class="nf">findViewById</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">custom_view_root</span><span class="p">)</span>
<span class="n">lockButton</span> <span class="p">=</span> <span class="n">myCustomView</span><span class="p">.</span><span class="nf">findViewById</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">lock_button</span><span class="p">)</span>
<span class="n">lockDescription</span> <span class="p">=</span> <span class="n">myCustomView</span><span class="p">.</span><span class="nf">findViewById</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">lock_status_description</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="assert-text-on-textviews">Assert Text on TextViews</h3>
<p>Asserting text on <code class="language-plaintext highlighter-rouge">TextView</code>’s is pretty straight forward by just using Junit’s basics <code class="language-plaintext highlighter-rouge">assertEquals</code>. Writing an extension function on <code class="language-plaintext highlighter-rouge">TextView</code> itself will also help writing this assertion more fluently.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">TextView</span><span class="p">.</span><span class="nf">assertText</span><span class="p">(</span><span class="n">expected</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`check</span> <span class="n">lock</span> <span class="n">text</span> <span class="nf">description`</span><span class="p">()</span> <span class="p">{</span>
<span class="n">lockDescription</span><span class="p">.</span><span class="nf">assertText</span><span class="p">(</span><span class="s">"Some expected text"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="assert-image-drawables">Assert Image Drawables</h3>
<p>Lets write a simple test now that will just assert a specific drawable is set on the <code class="language-plaintext highlighter-rouge">ImageButton</code> in our custom view as this is supposed to change when it is toggled. Robolectric has a function that will allow us to to check the resource id that a drawable was created from called <code class="language-plaintext highlighter-rouge">shadowOf(yourDrawable).createdFromResId</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`check</span> <span class="n">default</span> <span class="n">unlocked</span> <span class="n">state</span> <span class="n">of</span> <span class="n">image</span> <span class="nf">button`</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_open24px</span><span class="p">,</span> <span class="nf">shadowOf</span><span class="p">(</span><span class="n">lockButton</span><span class="p">.</span><span class="n">drawable</span><span class="p">).</span><span class="n">createdFromResId</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Since <code class="language-plaintext highlighter-rouge">ImageButton</code> extends <code class="language-plaintext highlighter-rouge">ImageView</code>, we can write another extension function on <code class="language-plaintext highlighter-rouge">ImageView</code> to clean up the syntax for asserting an image drawable on the lock button.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">ImageView</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nd">@DrawableRes</span> <span class="n">expected</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="nf">shadowOf</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">drawable</span><span class="p">).</span><span class="n">createdFromResId</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`check</span> <span class="n">default</span> <span class="n">unlocked</span> <span class="n">state</span> <span class="n">of</span> <span class="n">image</span> <span class="nf">button`</span><span class="p">()</span> <span class="p">{</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_24px</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="pass-custom-attributes-to-view">Pass Custom Attributes to View</h3>
<p>Robolectric has an <code class="language-plaintext highlighter-rouge">AttributeSetBuilder</code> that we can add our custom view attributes to and pass as the second argument to the view’s constructor. We will get rid of the <code class="language-plaintext highlighter-rouge">@Before</code> annotation on our setup function and will call this manually before each test so the initial default locked state can be configured for each test.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">private</span> <span class="kd">val</span> <span class="py">expectedUnlockText</span> <span class="p">=</span> <span class="s">"some unlock text"</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">expectedLockText</span> <span class="p">=</span> <span class="s">"some locked text"</span>
<span class="k">private</span> <span class="k">fun</span> <span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
<span class="o">..</span><span class="p">.</span>
<span class="kd">val</span> <span class="py">attributeSet</span> <span class="p">=</span> <span class="nf">with</span><span class="p">(</span><span class="nc">Robolectric</span><span class="p">.</span><span class="nf">buildAttributeSet</span><span class="p">())</span> <span class="p">{</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">unlockLabel</span><span class="p">,</span> <span class="n">expectedUnlockText</span><span class="p">)</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">lockLabel</span><span class="p">,</span> <span class="n">expectedLockText</span><span class="p">)</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">isLocked</span><span class="p">,</span> <span class="n">isLocked</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span>
<span class="nf">build</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">myCustomView</span> <span class="p">=</span> <span class="nc">MyCustomView</span><span class="p">(</span><span class="n">activity</span><span class="p">,</span> <span class="n">attributeSet</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`toggle</span> <span class="n">lock</span> <span class="p">-</span> <span class="n">should</span> <span class="n">be</span> <span class="nf">locked`</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
<span class="n">myCustomView</span><span class="p">.</span><span class="nf">toggleLock</span><span class="p">()</span>
<span class="n">lockDescription</span><span class="p">.</span><span class="nf">assertText</span><span class="p">(</span><span class="n">expectedLockText</span><span class="p">)</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_24px</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>One trick to clean up the builder function is to create our own function that has a parameter which is a function with a receiver. This will allow any attributes to be applied to the builder before building the <code class="language-plaintext highlighter-rouge">AttributeSet</code> avoiding the need to ever call the <code class="language-plaintext highlighter-rouge">build()</code> function directly in tests.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">buildAttributeSet</span><span class="p">(</span><span class="n">attrs</span><span class="p">:</span> <span class="nc">AttributeSetBuilder</span><span class="p">.()</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">):</span> <span class="nc">AttributeSet</span> <span class="p">{</span>
<span class="k">return</span> <span class="nf">with</span><span class="p">(</span><span class="nc">Robolectric</span><span class="p">.</span><span class="nf">buildAttributeSet</span><span class="p">())</span> <span class="p">{</span>
<span class="nf">attrs</span><span class="p">()</span>
<span class="nf">build</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">fun</span> <span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">attributeSet</span> <span class="p">=</span> <span class="nf">buildAttributeSet</span> <span class="p">{</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">unlockLabel</span><span class="p">,</span> <span class="n">expectedUnlockText</span><span class="p">)</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">lockLabel</span><span class="p">,</span> <span class="n">expectedLockText</span><span class="p">)</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">isLocked</span><span class="p">,</span> <span class="n">isLocked</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span>
<span class="p">}</span>
<span class="n">myCustomView</span> <span class="p">=</span> <span class="nc">MyCustomView</span><span class="p">(</span><span class="n">activity</span><span class="p">,</span> <span class="n">attributeSet</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="testing-view-listeners">Testing View Listeners</h3>
<p>Most views have some kind of listener when states are changed and in the instance of <code class="language-plaintext highlighter-rouge">MyCustomView</code>, it has a listener that is triggered when ever the user changes the lock state. So in order to write this kind of test, a mocking library is needed and we will use <a href="https://mockk.io">Mockk</a> to make these verifications.</p>
<p>Add Mockk to your module’s dependencies:</p>
<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">dependencies</span> <span class="o">{</span>
<span class="n">testImplementation</span> <span class="s2">"io.mockk:mockk:1.10.3-jdk8"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Create a mocked lambda in the test and have it return <code class="language-plaintext highlighter-rouge">Unit</code> anytime it is invoked in our <code class="language-plaintext highlighter-rouge">setup()</code> function. Then you can set the listener on the custom view and write the toggle listener test.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">private</span> <span class="kd">val</span> <span class="py">lockedListener</span><span class="p">:</span> <span class="p">(</span><span class="nc">Boolean</span><span class="p">)</span> <span class="p">-></span> <span class="nc">Unit</span> <span class="p">=</span> <span class="nf">mockk</span><span class="p">()</span>
<span class="k">private</span> <span class="k">fun</span> <span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">every</span> <span class="p">{</span> <span class="nf">lockedListener</span><span class="p">(</span><span class="nf">any</span><span class="p">())</span> <span class="p">}</span> <span class="n">returns</span> <span class="nc">Unit</span>
<span class="n">myCustomView</span><span class="p">.</span><span class="n">onLockListener</span> <span class="p">=</span> <span class="n">lockedListener</span>
<span class="p">}</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`lock</span> <span class="n">listener</span> <span class="n">invoked</span> <span class="p">-</span> <span class="n">initial</span> <span class="k">false</span> <span class="n">then</span> <span class="n">toggled</span> <span class="n">to</span> <span class="k">true</span><span class="err">`</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
<span class="n">myCustomView</span><span class="p">.</span><span class="nf">toggleLock</span><span class="p">()</span>
<span class="nf">verify</span> <span class="p">{</span> <span class="nf">lockedListener</span><span class="p">(</span><span class="k">true</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="testing-background-drawables">Testing Background Drawables</h3>
<p>One requirement that was set for this custom view is that when the view is locked it should only be unlocked by clicking on the lock button itself and not whole card. This can be achieved by removing the ripple on the background drawable to indicate to the user it is not clickable and verifying that our toggle listener is not invoked with Mockk.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">View</span><span class="p">.</span><span class="nf">assertBackground</span><span class="p">(</span><span class="nd">@DrawableRes</span> <span class="n">expected</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">assertEquals</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="nf">shadowOf</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">background</span><span class="p">).</span><span class="n">createdFromResId</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`root</span> <span class="n">shouldn</span><span class="err">'</span><span class="n">t</span> <span class="n">have</span> <span class="n">ripple</span> <span class="k">when</span> <span class="n">locked</span> <span class="n">and</span> <span class="n">only</span> <span class="n">unlock</span> <span class="n">with</span> <span class="n">image</span> <span class="nf">button`</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">setUp</span><span class="p">(</span><span class="n">isLocked</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="n">myCustomView</span><span class="p">.</span><span class="nf">performClick</span><span class="p">()</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_24px</span><span class="p">)</span>
<span class="n">rootView</span><span class="p">.</span><span class="nf">assertBackground</span><span class="p">(</span><span class="n">android</span><span class="p">.</span><span class="nc">R</span><span class="p">.</span><span class="n">color</span><span class="p">.</span><span class="n">white</span><span class="p">)</span>
<span class="nf">verify</span><span class="p">(</span><span class="n">exactly</span> <span class="p">=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nf">lockedListener</span><span class="p">(</span><span class="nf">any</span><span class="p">())</span> <span class="p">}</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">performClick</span><span class="p">()</span>
<span class="n">rootView</span><span class="p">.</span><span class="nf">assertBackground</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">my_custom_ripple</span><span class="p">)</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_open_24px</span><span class="p">)</span>
<span class="nf">verify</span> <span class="p">{</span> <span class="nf">lockedListener</span><span class="p">(</span><span class="k">false</span><span class="p">)</span> <span class="p">}</span>
<span class="n">myCustomView</span><span class="p">.</span><span class="nf">performClick</span><span class="p">()</span>
<span class="n">rootView</span><span class="p">.</span><span class="nf">assertBackground</span><span class="p">(</span><span class="n">android</span><span class="p">.</span><span class="nc">R</span><span class="p">.</span><span class="n">color</span><span class="p">.</span><span class="n">white</span><span class="p">)</span>
<span class="n">lockButton</span><span class="p">.</span><span class="nf">assertDrawableResource</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_lock_24px</span><span class="p">)</span>
<span class="nf">verify</span> <span class="p">{</span> <span class="nf">lockedListener</span><span class="p">(</span><span class="k">true</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="passing-drawables-through-attributeset">Passing Drawables through AttributeSet</h3>
<p>This would probably be overkill for this custom view, but if you ever wanted to pass any drawable resource through the custom view attributes I thought it would be worth mentioning how to do this as it may help generalize how to pass anything through the <code class="language-plaintext highlighter-rouge">AttributeSetBuilder</code>. First add the new attributes to the custom view styleable and use the attributes on <code class="language-plaintext highlighter-rouge">MyCustomView</code>.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><resources></span>
<span class="nt"><declare-styleable</span> <span class="na">name=</span><span class="s">"MyCustomView"</span><span class="nt">></span>
<span class="nt"><attr</span> <span class="na">name=</span><span class="s">"lockedIcon"</span> <span class="na">format=</span><span class="s">"reference"</span><span class="nt">/></span>
<span class="nt"><attr</span> <span class="na">name=</span><span class="s">"unlockedIcon"</span> <span class="na">format=</span><span class="s">"reference"</span><span class="nt">/></span>
<span class="nt"></declare-styleable></span>
<span class="nt"></resources></span>
</code></pre></div></div>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt"><com.plusmobileapps.plusmobileappsui.MyCustomView</span>
<span class="err">...</span>
<span class="na">app:lockedIcon=</span><span class="s">"@drawable/ic_lock_24px"</span>
<span class="na">app:unlockedIcon=</span><span class="s">"@drawable/ic_lock_open_24px"</span> <span class="nt">/></span>
</code></pre></div></div>
<p>Since the only value that can be added as an attribute to the builder is a string, you actually need to pass the exact string used to declare the drawable used in the custom view declaration.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">attributeSet</span> <span class="p">=</span> <span class="nf">buildAttributeSet</span> <span class="p">{</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">lockedIcon</span><span class="p">,</span> <span class="s">"@drawable/ic_lock_24px"</span><span class="p">)</span>
<span class="nf">addAttribute</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">attr</span><span class="p">.</span><span class="n">unlockedIcon</span><span class="p">,</span> <span class="s">"@drawable/ic_lock_open_24px"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>So as a general rule of thumb, anything that needs to be added to the builder is the exact string you would use if you were to declare it in xml.</p>
<p>All of the work needed to add this functionality can be found in this <a href="https://github.com/plusmobileapps/test-custom-android-view/commit/fb84c862fa22825795e12b8df82cfe221526aa03">commit</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>As we come to the end of this post, I hope you learned how to use Robolectric to unit test the logic in a custom Android view in a variety of different scenarios. Robolectric is a great tool in any Android developers tool set unlocking the ability to test Android locally on your machine without needing to run a slow instrumented test with Espresso. There are a lot of other things Robolectric can be used for when testing Android and we just touched the tip of the iceberg in this post. So I encourage you to explore the <a href="https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/Robolectric.java"><code class="language-plaintext highlighter-rouge">Robolectric</code></a> class to see what else is possible. Happy coding!</p>
<h2 id="source-code">Source code</h2>
<ul>
<li><a href="https://github.com/plusmobileapps/test-custom-android-view">Github Repository</a></li>
<li><a href="https://github.com/plusmobileapps/test-custom-android-view/blob/master/plusmobileappsui/src/test/java/com/plusmobileapps/plusmobileappsui/MyCustomViewTest.kt"><code class="language-plaintext highlighter-rouge">MyCustomViewTest</code></a></li>
<li><a href="https://github.com/plusmobileapps/test-custom-android-view/blob/master/plusmobileappsui/src/main/java/com/plusmobileapps/plusmobileappsui/MyCustomView.kt"><code class="language-plaintext highlighter-rouge">MyCustomView</code></a></li>
</ul>Andrew Steinmetzandrew@plusmobileappsWorking at an enterprise that needs a custom component library with each component being its own snow flake and having more complicated logic than the next. I found myself needing a way to test the logic in these views to ensure I could iterate quickly and not break anything in the process.How to build a Slackbot with Kotlin2020-10-09T00:00:00+00:002020-10-09T00:00:00+00:00https://plusmobileapps.com/2020/10/09/ktor-slackbot-heroku<p>One day at my day job, I noticed that there were a couple of services we used that anytime an event happened there was a very manual process for the developer to copy paste data into a Slack channel for others to be aware of the issue. I thought there had to be a better way to automate this whole process and prevent a developer from having to do this tedious task. That was when I came up with the idea of creating a Slackbot application to do this. So in this article I will describe the process I went through to create a Slackbot using Ktor and webhooks that can post messages to your Slack channel of choice and how to deploy to Heroku. For this example, we will be using <a href="https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/about-webhooks">Github webhooks</a> to supply data to our Slackbot but the same principal applies to your service of choice that offers webhooks.</p>
<!--more-->
<h2 id="setup">Setup</h2>
<h3 id="initialize-project">Initialize Project</h3>
<ol>
<li>First download <a href="https://www.jetbrains.com/idea/download/download-thanks.html?&code=IIC">IntelliJ IDEA CE</a> and install for your development platform.</li>
<li>Once installed, click configure -> plugins. Then search and install the Ktor plugin.</li>
</ol>
<p><img src="/assets/images/intellij-extension.png" alt="" /></p>
<ol>
<li>Create a new project and select Ktor in the side bar. For the sake of this tutorial, we are going to use the GSON library for JSON serialization. But this is swappable with other JSON serializers of your choosing.</li>
</ol>
<p><img src="/assets/images/create-ktor-project.png" alt="" /></p>
<p>After giving your project a name and finishing the setup, you should be able to run the project with the following command and open up your browser to <a href="http://0.0.0.0:8080">http://0.0.0.0:8080</a>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradle run
</code></pre></div></div>
<p><img src="/assets/images/hello-world-ktor.png" alt="" /></p>
<h3 id="create-a-slack-bot-app">Create a Slack Bot App</h3>
<p>Go to <a href="https://api.slack.com/apps">Slack’s API website</a> and create a new app. Give a name for your bot and the workspace that this bot will have access to.</p>
<p><img src="/assets/images/slack-app-create.png" alt="" /></p>
<p>Under OAuth and Permissions, we need to give our bot the permission to write to our Slack channels so go ahead and add the following permission.</p>
<p><img src="/assets/images/slack-bot-permissions.png" alt="" /></p>
<p>At the top of this page, you should now be able to install this bot to your workspace.</p>
<p><img src="/assets/images/install-slack-bot.png" alt="" /></p>
<p>Now that you have installed the bot to the workspace, it should land you back on OAuth and Permissions page with a token that we will use later to authenticate with our Slack instance to post messages. This key should be kept private and not checked into any repository which we will discuss in a bit how to keep this secret.</p>
<p><img src="/assets/images/slack-bot-token.png" alt="" /></p>
<h3 id="add-java-slack-sdk">Add Java Slack SDK</h3>
<p>Now we will add the <a href="https://github.com/slackapi/java-slack-sdk">Java Slack SDK</a> to our project. First open up the <code class="language-plaintext highlighter-rouge">build.gradle</code> file, and add the following three lines to the dependencies block.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">implementation</span> <span class="s2">"com.slack.api:slack-api-client:$slack_version"</span>
<span class="c1">// Add these dependencies if you want to use the Kotlin DSL for building rich messages</span>
<span class="n">implementation</span> <span class="s2">"com.slack.api:slack-api-model-kotlin-extension:$slack_version"</span>
<span class="n">implementation</span> <span class="s2">"com.slack.api:slack-api-client-kotlin-extension:$slack_version"</span>
</code></pre></div></div>
<p>Then in <code class="language-plaintext highlighter-rouge">gradle.properties</code> we can create the variable for the version.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">slack_version</span><span class="o">=</span><span class="mf">1.2</span><span class="o">.</span><span class="mi">1</span>
</code></pre></div></div>
<p>Now sync gradle and the Slack Java SDK should be accessible. If you have any trouble setting this up, the official instructions from Slack can be found <a href="https://slack.dev/java-slack-sdk/guides/web-api-client-setup">here</a>.</p>
<h2 id="posting-messages">Posting Messages</h2>
<p>Before we can post any messages, lets now add the Slack bot token to our project as an environment variable since you should never be checking in api tokens directly to your repository. Click on the gradle configuration in the top toolbar, and edit configurations. Then click on the right icon of environment variables and click the plus button to add an environment variable.</p>
<p><img src="/assets/images/slack-token-env-variable.png" alt="" /></p>
<p>Our bot doesn’t have permissions to join channels, so to prevent the <code class="language-plaintext highlighter-rouge">not_in_channel</code> error add the bot by @ it in the slack channel you want to post a message to. <code class="language-plaintext highlighter-rouge">@Plus Mobile Apps Slack Bot</code> in the <code class="language-plaintext highlighter-rouge">#general</code> channel.</p>
<p>To create a simple message, lets create a new file called <code class="language-plaintext highlighter-rouge">HomeRoute</code>. Here we will create an extension function on <code class="language-plaintext highlighter-rouge">Route</code> which will post a simple message anytime the home page is retrieved.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Routing</span><span class="p">.</span><span class="nf">homeRoute</span><span class="p">()</span> <span class="p">{</span>
<span class="k">get</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">token</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(</span><span class="s">"SLACK_TOKEN"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">slack</span> <span class="p">=</span> <span class="nc">Slack</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">slack</span><span class="p">.</span><span class="nf">methods</span><span class="p">(</span><span class="n">token</span><span class="p">).</span><span class="nf">chatPostMessage</span> <span class="p">{</span>
<span class="n">it</span><span class="p">.</span><span class="nf">channel</span><span class="p">(</span><span class="s">"#general"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">text</span><span class="p">(</span><span class="s">"Hello :wave:"</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">call</span><span class="p">.</span><span class="nf">respondText</span><span class="p">(</span><span class="s">"Response is: $response"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Don’t forget to add this function to your routes on the <code class="language-plaintext highlighter-rouge">Application.kt</code> file.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Application</span><span class="p">.</span><span class="nf">module</span><span class="p">(</span><span class="n">testing</span><span class="p">:</span> <span class="nc">Boolean</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">routing</span> <span class="p">{</span>
<span class="nf">homeRoute</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now run the gradle configuration in IntelliJ and go <code class="language-plaintext highlighter-rouge">http://0.0.0.0:8080</code>. This route should now trigger a message being sent to the general channel.</p>
<p><img src="/assets/images/simple-slackbot-message.png" alt="" /></p>
<h2 id="deploy-to-heroku">Deploy to Heroku</h2>
<p>If you don’t have a Heroku account yet, go to the <a href="https://www.heroku.com">website</a> and create one. Once signed in, create a new app in the top right and give it a name of your choice. Then you will need to <a href="https://devcenter.heroku.com/articles/heroku-cli">install the Heroku CLI</a> in order to push your code. Now login to your Heroku account through the CLI and add the Heroku git remote. (This name should be under the deploy tab on Heroku if you forget the name of of your app.)</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>heroku login
heroku git:remote <span class="nt">-a</span> ktor-slack-bot
</code></pre></div></div>
<p>Now go into the settings of your Heroku app and we will add the Slack token from earlier.</p>
<p><img src="/assets/images/heroku-env-variable.png" alt="" /></p>
<p>Heroku itself is not always going to run on port 8080 and actually passes this port number as an environment variable, so the application needs to be tweaked to take a <code class="language-plaintext highlighter-rouge">PORT</code> variable.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="p">:</span> <span class="nc">Array</span><span class="p"><</span><span class="nc">String</span><span class="p">>)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">port</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(</span><span class="s">"PORT"</span><span class="p">).</span><span class="nf">toIntOrNull</span><span class="p">()</span> <span class="o">?:</span> <span class="mi">8080</span>
<span class="nf">embeddedServer</span><span class="p">(</span><span class="nc">Netty</span><span class="p">,</span> <span class="n">port</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">routing</span> <span class="p">{</span>
<span class="nf">homeRoute</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}.</span><span class="nf">start</span><span class="p">(</span><span class="n">wait</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now in the <code class="language-plaintext highlighter-rouge">build.gradle</code> file, the entry point to the app needs to be updated with the <code class="language-plaintext highlighter-rouge">mainClassName</code>. We are also going to add two more tasks while we are here called <code class="language-plaintext highlighter-rouge">jar</code> and <code class="language-plaintext highlighter-rouge">stage</code>. Jar will package the app into an executable file and stage is needed by Heroku which is run when deploying your app.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mainClassName</span> <span class="o">=</span> <span class="s2">"com.plusmobileapps.ApplicationKt"</span>
<span class="n">jar</span> <span class="o">{</span>
<span class="n">manifest</span> <span class="o">{</span>
<span class="n">attributes</span> <span class="s1">'Main-Class'</span> <span class="o">:</span> <span class="s2">"com.plusmobileapps.ApplicationKt"</span>
<span class="o">}</span>
<span class="n">from</span> <span class="o">{</span> <span class="n">configurations</span><span class="o">.</span><span class="na">runtimeClasspath</span><span class="o">.</span><span class="na">collect</span> <span class="o">{</span> <span class="n">it</span><span class="o">.</span><span class="na">isDirectory</span><span class="o">()</span> <span class="o">?</span> <span class="n">it</span> <span class="o">:</span> <span class="n">zipTree</span><span class="o">(</span><span class="n">it</span><span class="o">)</span> <span class="o">}</span> <span class="o">}</span>
<span class="o">}</span>
<span class="n">task</span> <span class="n">stage</span> <span class="o">{</span>
<span class="n">dependsOn</span> <span class="n">jar</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Next, there is a special file we will make in the root of our project called <a href="https://devcenter.heroku.com/articles/procfile"><code class="language-plaintext highlighter-rouge">Procfile</code></a>, this is what tells Heroku how to run our app once it is deployed. Add the following to this file which will run the jar file that is generated from the stage task.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>web: java -jar ./build/libs/slackbot-0.0.1.jar
</code></pre></div></div>
<p>To <a href="https://devcenter.heroku.com/articles/heroku-local">run Heroku locally</a>, create a <code class="language-plaintext highlighter-rouge">.env</code> file in the root of the project folder where we will specify the <code class="language-plaintext highlighter-rouge">SLACK_TOKEN</code> and <code class="language-plaintext highlighter-rouge">PORT</code> variables. Don’t forget to add the <code class="language-plaintext highlighter-rouge">.env</code> file to your <code class="language-plaintext highlighter-rouge">.gitignore</code> file to avoid checking in your secrets to your version control.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SLACK_TOKEN=your-token-here
PORT=8080
</code></pre></div></div>
<p>Then run the stage task and Heroku local command.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradle stage
heroku <span class="nb">local</span>
</code></pre></div></div>
<p>After validating everything is working locally, now you can finally deploy your Slackbot to Heroku by pushing it to the Heroku remote which will kick off the stage task. The url where your app is deployed will be printed in the console at the end of the deployment. So for this app, if you type <code class="language-plaintext highlighter-rouge">https://ktor-slack-bot.herokuapp.com/</code> into your browser you will kick off the simple message to the #general channel from the Slackbot.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push heroku master
Enumerating objects: 10, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>10/10<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Delta compression using up to 16 threads
Compressing objects: 100% <span class="o">(</span>5/5<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>6/6<span class="o">)</span>, 782 bytes | 782.00 KiB/s, <span class="k">done</span><span class="nb">.</span>
Total 6 <span class="o">(</span>delta 3<span class="o">)</span>, reused 0 <span class="o">(</span>delta 0<span class="o">)</span>
remote: Compressing <span class="nb">source </span>files... <span class="k">done</span><span class="nb">.</span>
remote: Building <span class="nb">source</span>:
remote:
remote: <span class="nt">-----</span><span class="o">></span> Gradle app detected
remote: <span class="nt">-----</span><span class="o">></span> Installing JDK 1.8... <span class="k">done
</span>remote: <span class="nt">-----</span><span class="o">></span> Building Gradle app...
remote: <span class="nt">-----</span><span class="o">></span> executing ./gradlew stage
remote:
remote: <span class="o">></span> Task :compileKotlin
remote: w: /tmp/build_c370990d/src/Application.kt: <span class="o">(</span>16, 24<span class="o">)</span>: Parameter <span class="s1">'testing'</span> is never used
remote:
remote: <span class="o">></span> Task :compileJava NO-SOURCE
remote: <span class="o">></span> Task :processResources
remote: <span class="o">></span> Task :classes
remote: <span class="o">></span> Task :inspectClassesForKotlinIC
remote: <span class="o">></span> Task :jar
remote: <span class="o">></span> Task :stage
remote:
remote: BUILD SUCCESSFUL <span class="k">in </span>21s
remote: 4 actionable tasks: 4 executed
remote: <span class="nt">-----</span><span class="o">></span> Discovering process types
remote: Procfile declares types -> web
remote:
remote: <span class="nt">-----</span><span class="o">></span> Compressing...
remote: Done: 67.9M
remote: <span class="nt">-----</span><span class="o">></span> Launching...
remote: Released v5
remote: https://ktor-slack-bot.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... <span class="k">done</span><span class="nb">.</span>
To https://git.heroku.com/ktor-slack-bot.git
+ 6032313...ab7fa93 master -> master <span class="o">(</span>forced update<span class="o">)</span>
</code></pre></div></div>
<p>All of the changes needed to deploy can be found in this <a href="https://github.com/plusmobileapps/kotlin-slackbot/commit/d0f1c6f0bf20ac267409f7fac98eec032080bef4">commit</a>.</p>
<h2 id="listen-for-web-hooks-to-post-messages">Listen for web hooks to post messages</h2>
<p>The application until this point is pretty simple, but now that the application is deployed there is a URL you can use to register for web hooks. For this example, we will use Github web hooks from this repository which you can register in the settings section of the repository -> web hooks.</p>
<p><img src="/assets/images/slackbot-add-webhook.png" alt="" /></p>
<p>To test what data this endpoint will be receiving, I use a service called <a href="https://beeceptor.com">Beeceptor</a> which will set up an endpoint of your choice and will display the JSON that is sent from the webhook. After creating an endpoint on Beeceptor, register the url from Beeceptor as a webhook for the repository in order to see what JSON is sent in the payload. When you are first setting up a Github webhook, there are two requests that will be sent.</p>
<ol>
<li><a href="https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#ping">Ping</a> - sent when the webhook is first setup correctly</li>
<li><a href="https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#push">Push</a> - sent whenever a commit is pushed to the repository</li>
</ol>
<p>Both of these JSON payloads are saved into the <a href="https://github.com/plusmobileapps/kotlin-slackbot/tree/master/json">json/</a> folder of the repository.</p>
<p>Now create a new route in the application that will receive the webhook event, deserialize the JSON, and then post a message to Slack with the information we want. <a href="https://github.com/plusmobileapps/kotlin-slackbot/blob/3f08c002b58c99ab37f1613eb79d7b20440ab213/src/GithubPushEvent.kt"><code class="language-plaintext highlighter-rouge">GithubPushEvent</code></a> is just the JSON from the payload represented as a Kotlin class. Do not forget to add this extension function to your <code class="language-plaintext highlighter-rouge">Application#route</code> block.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Route</span><span class="p">.</span><span class="nf">githubWebhookRoute</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">post</span><span class="p">(</span><span class="s">"kotlin-slackbot-github"</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">request</span> <span class="p">=</span> <span class="n">call</span><span class="p">.</span><span class="n">receive</span><span class="p"><</span><span class="nc">GithubPushEvent</span><span class="p">>()</span>
<span class="kd">val</span> <span class="py">token</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(</span><span class="s">"SLACK_TOKEN"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">slack</span> <span class="p">=</span> <span class="nc">Slack</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">slack</span><span class="p">.</span><span class="nf">methods</span><span class="p">(</span><span class="n">token</span><span class="p">).</span><span class="nf">chatPostMessage</span> <span class="p">{</span>
<span class="n">it</span><span class="p">.</span><span class="nf">channel</span><span class="p">(</span><span class="s">"#general"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">text</span><span class="p">(</span><span class="s">"""
New commit pushed to `${request.repository.full_name}` by ${request.pusher.name}
> ${request.head_commit.message}
${request.head_commit.url}
"""</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">())</span>
<span class="p">}</span>
<span class="n">call</span><span class="p">.</span><span class="nf">respondText</span><span class="p">(</span><span class="s">"Response is: $response"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>To test locally, run the application and use your tool of choice to send a post request. I use Postman and send the post request to the application’s local host with the JSON payload saved earlier from the push event.</p>
<p><img src="/assets/images/postman-slackbot.png" alt="" /></p>
<p>After everything is working locally, go ahead and deploy to Heroku. The end result of the message posted to Slack whenever a commit is pushed to the repository!</p>
<p><img src="/assets/images/final-slackbot-message.png" alt="" /></p>
<h2 id="prevent-blocking-thread">Prevent blocking thread</h2>
<p>Before we are done, if you look closely at the posting of the message there is a warning from the IDE that there is an inappropriate blocking method call. Since Ktor uses coroutines, the easiest solution to fix this is to simply move this call off of the original thread using <code class="language-plaintext highlighter-rouge">withContext()</code> and indicating which Dispatcher this should be run on.</p>
<p><img src="/assets/images/blocking-thread.png" alt="" /></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">Route</span><span class="p">.</span><span class="nf">githubWebhookRoute</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">post</span><span class="p">(</span><span class="s">"kotlin-slackbot-github"</span><span class="p">)</span> <span class="p">{</span>
<span class="o">..</span><span class="p">.</span>
<span class="kd">val</span> <span class="py">responses</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p"><</span><span class="nc">Any</span><span class="p">>()</span>
<span class="nf">withContext</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">slack</span><span class="p">.</span><span class="nf">methods</span><span class="p">(</span><span class="n">token</span><span class="p">).</span><span class="nf">chatPostMessage</span> <span class="p">{</span>
<span class="n">it</span><span class="p">.</span><span class="nf">channel</span><span class="p">(</span><span class="s">"#kotlin-slackbot"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">text</span><span class="p">(</span>
<span class="s">"""
New commit pushed to `${request.repository.full_name}` by ${request.pusher.name}
> ${request.head_commit.message}
${request.head_commit.url}
"""</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">()</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="n">responses</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">call</span><span class="p">.</span><span class="nf">respondText</span><span class="p">(</span><span class="s">"Response is: $responses"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now the warning is gone and the application will no longer be blocking the main thread when the endpoint is hit.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, we have created a Slack application using Kotlin, Ktor, and Heroku that can listen to webhook events and post messages to our Slack. Everything done here can be found in the <a href="https://github.com/plusmobileapps/kotlin-slackbot">Github repository</a>. We just scratched the tip of the iceberg in this article, but the Slack API is very powerful and encourage you to explore all of the different <a href="https://api.slack.com/methods">methods</a> you can use to create a powerful Slackbot. Happy coding!</p>Andrew Steinmetzandrew@plusmobileappsOne day at my day job, I noticed that there were a couple of services we used that anytime an event happened there was a very manual process for the developer to copy paste data into a Slack channel for others to be aware of the issue. I thought there had to be a better way to automate this whole process and prevent a developer from having to do this tedious task. That was when I came up with the idea of creating a Slackbot application to do this. So in this article I will describe the process I went through to create a Slackbot using Ktor and webhooks that can post messages to your Slack channel of choice and how to deploy to Heroku. For this example, we will be using Github webhooks to supply data to our Slackbot but the same principal applies to your service of choice that offers webhooks.Create a Website with Github Pages2020-05-06T00:00:00+00:002020-05-06T00:00:00+00:00https://plusmobileapps.com/2020/05/06/create-gh-pages-site<p><img src="/assets/images/material-mkdocs.png" alt="" /></p>
<p>If you have a project, blog, or documentation that you need to have hosted in a site, <a href="https://pages.github.com/">Github Pages</a> can be a great way to host your site for free straight from the repository.</p>
<!--more-->
<p>Since this website itself was made with <a href="https://github.com/squidfunk/mkdocs-material">Material MkDocs</a>, this tutorial will explain how to build and deploy a static website to Github Pages using that.</p>
<h2 id="install-python">Install Python</h2>
<p>MkDocs is built using Python, so we will need to install Python first in order to build and run anything. Since Macs by default have Python 2 installed, we will need to install Python 3 to use MkDocs. The easiest way to install Python 3 is with <a href="https://brew.sh/">Homebrew</a>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>python
</code></pre></div></div>
<p>Once installed, you should be able to verify this by running the following command to see the version number.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python3 <span class="nt">-V</span>
Python 3.7.7
</code></pre></div></div>
<p>If you wish to use the <code class="language-plaintext highlighter-rouge">python</code> command in the terminal and have it reference the <code class="language-plaintext highlighter-rouge">python3</code> version. Following this <a href="https://opensource.com/article/19/5/python-3-default-mac">article</a>, we can create an alias in your bash profile that will reference the <code class="language-plaintext highlighter-rouge">python3</code> version. First get the path of where <code class="language-plaintext highlighter-rouge">python3</code> was installed.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>which python3
/usr/local/bin/python3
</code></pre></div></div>
<p>Now add an alias to your bash profile file that will now make any <code class="language-plaintext highlighter-rouge">python</code> command in the terminal point to the <code class="language-plaintext highlighter-rouge">python3</code> version instead of Mac default of <code class="language-plaintext highlighter-rouge">python2</code>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">python</span><span class="o">=</span>/usr/local/bin/python3
</code></pre></div></div>
<p>After sourcing your bash profile with the new alias, you should now be able to just use <code class="language-plaintext highlighter-rouge">python</code> in the terminal instead <code class="language-plaintext highlighter-rouge">python3</code>.</p>
<h2 id="download-and-install-material-mkdocs">Download and Install Material MkDocs</h2>
<h3 id="using-github">Using Github</h3>
<p>The easiest way to get running is to fork the repository from Github and then clone that repository to your machine.</p>
<p><img src="/assets/images/fork-repository.png" alt="Fork Github Repository" /></p>
<p>Now in order to access your website in the public space in the end, we must rename the repository to <code class="language-plaintext highlighter-rouge"><username>.github.io</code> in order to work with the default domain given for free from Github Pages. Go to the settings of your repository, and rename the repo with your username replaced.</p>
<p><img src="/assets/images/rename-repo.png" alt="Rename Forked Repository" /></p>
<p>Now we can clone the repository to our local machine with git.</p>
<p>!!! note
I am using the the SSH urls when cloning, you could just as easily use the https urls. If you have not set that up in git and wish to learn how, checkout <a href="/../dev-basics/ssh">how to use SSH</a>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`git clone https://github.com/squidfunk/mkdocs-material.git`
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:plusmobileapps/plusmobileapps.github.io.git
</code></pre></div></div>
<h3 id="enterprise-github">Enterprise Github</h3>
<p>In case you are working with an enterprise instance of Github that has a different domain and is not publicly accessible without some form of authentication. Then just go ahead and clone the Material MkDocs repository directly and add your enterprise’s remote url.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:squidfunk/mkdocs-material.git
git remote add enterprise https://enterprise.repo.forforking.com/user/repo.git
git push enterprise master
</code></pre></div></div>
<h2 id="running-material-mkdocs-locally">Running Material MkDocs Locally</h2>
<p>First install all of the Material MkDocs dependencies with Python’s package manager, <code class="language-plaintext highlighter-rouge">pip</code>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>mkdocs-material
</code></pre></div></div>
<p>Then to run the server, you can run the following command on the <code class="language-plaintext highlighter-rouge">mkdocs</code> python module.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> mkdocs serve
</code></pre></div></div>
<p>Open up your browser and navigate to <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/</code> and you should now see the landing page for your Material MkDocs site.</p>
<p><img src="/assets/images/mkdocs-home.png" alt="Material MkDocs" /></p>
<p>!!! note
If you run into an error when running the command above with the following</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```bash
MkDocs encountered as error parsing the configuration file: while constructing a Python object
cannot find module 'materialx.emoji' (No module named 'materialx')
```
Then comment out the following lines in the `mkdocs.yml` file since you probably don't need the emoji dependency right away.
```yml
# - pymdownx.emoji:
# emoji_index: !!python/name:materialx.emoji.twemoji
# emoji_generator: !!python/name:materialx.emoji.to_svg
```
</code></pre></div></div>
<h2 id="deploying-your-site-to-github-pages">Deploying Your Site To Github Pages</h2>
<h3 id="deploying-to-github">Deploying to Github</h3>
<p>To access your site publicly from any broswer, we can now deploy it with the following command.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> mkdocs gh-deploy
</code></pre></div></div>
<p>Be sure that the repository settings is configured for Github Pages to be built off of the <code class="language-plaintext highlighter-rouge">gh-pages</code> branch as this is the default branch MkDocs will deploy to.</p>
<p><img src="/assets/images/gh-pages-branch.png" alt="" /></p>
<p>If you are creating a personal Github Pages website for your username and do not see the option to switch the branch. This is because it must be built off of master which is an easy fix to deploy to.</p>
<p><img src="/assets/images/personal-gh-pages.png" alt="" /></p>
<p>From the master branch, checkout a new branch and call it <code class="language-plaintext highlighter-rouge">develop</code> then push it to Github. Now you can configure MkDocs to deploy to the <code class="language-plaintext highlighter-rouge">master</code> branch instead of the default <code class="language-plaintext highlighter-rouge">gh-pages</code> branch.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout <span class="nt">-b</span> develop
git push origin develop
python3 <span class="nt">-m</span> mkdocs gh-deploy <span class="nt">-b</span> master
</code></pre></div></div>
<h3 id="deploying-to-github-enterprise">Deploying to Github Enterprise</h3>
<p>To deploy to your enterprise instance of Github, you must make use of the remote flag to tell MkDocs that it should deploy to the <code class="language-plaintext highlighter-rouge">gh-pages</code> branch on your remote repository.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> mkdocs gh-deploy <span class="nt">-r</span> myfork
</code></pre></div></div>
<p>The easiest way to figure out your url for you enterprise Github Pages site is to go to the repository’s settings, and go down to the Github Pages section to see where it was published.</p>
<p><img src="/assets/images/enterprise-ghpages-name.png" alt="Enterprise Github Pages Name" /></p>
<h2 id="configure-custom-domain-for-github-pages">Configure Custom Domain for Github Pages</h2>
<p><a href="https://help.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site">Managing Custom Domain - Github Docs</a></p>
<p><a href="https://dev.to/trentyang/how-to-setup-google-domain-for-github-pages-1p58">Dev.to article</a> for configuring Github Pages with custom domain on Google Domains.</p>
<p><a href="https://www.mkdocs.org/user-guide/deploying-your-docs/#custom-domains">Deploying MkDocs CNAME</a> - adding a <a href="https://github.com/plusmobileapps/plusmobileapps.github.io/blob/develop/docs/CNAME">CNAME file</a> in the docs folder that contains the domain name that was used in the custom domain field in the repository settings will allow the <code class="language-plaintext highlighter-rouge">mkdocs gh-deploy</code> command from wiping out the CNAME file in the master branch.</p>
<p><img src="/assets/images/gh-custom-domain.png" alt="" /></p>
<p>If you happen to get the following warning when updating the custom domain in your Github repository settings. I found out there was another repository on my account that had the custom domain already setup and deleting that custom domain on the other repository fixed my issue.</p>
<p><img src="/assets/images/github-pages-error.png" alt="" /></p>Andrew Steinmetzandrew@plusmobileappsIf you have a project, blog, or documentation that you need to have hosted in a site, Github Pages can be a great way to host your site for free straight from the repository.Physical Workspace2020-03-02T00:00:00+00:002020-03-02T00:00:00+00:00https://plusmobileapps.com/2020/03/02/physical-workspace<p>One aspect of a programmer’s work that I think can get overlooked is the actual physical workspace where he or she works. Over the years I have invested a lot of time and effort into creating a home office that is ergonomic and promotes a workflow that works for me. By no means am I trying to tell everyone that…</p>
<p><img src="/assets/images/this-is-the-way.gif" alt="This is the way" /></p>
<!--more-->
<p>and you shouldn’t get anything else. But I do recommend that you invest in the similar items for each section listed to help create your own environment that will help you make yourself more productive.</p>
<h2 id="desk">Desk</h2>
<p>For a desk, I highly recommend some sort of standing desk that can be adjusted throughout the day so you are not always sitting down the entire day. I personally have an <a href="https://www.upliftdesk.com/uplift-v2-standing-desk-v2-or-v2-commercial/">Uplift V2 Standing Desk</a> which has been a great investment. If you don’t have the money for a standing desk, I have gone the cheapest route before with just a little Ikea end table I put on top of my desk that takes a minute to setup and just put my laptop on top of it. I cannot stress the importance of alternating between standing and sitting throughout the day to try to maintain your posture.</p>
<p>In case you were interested in the Uplift standing desk, below is a video describing all of the different customizations that you can make to create a standing desk that works for you. I am not paid to praise their desks, just a happy customer of Uplift.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/8krBbc31gBM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
<h2 id="keyboard">Keyboard</h2>
<p>Now when it comes to a keyboard, I have joined the cult of mechanical keyboards as I truly love the feel of every click of a key when typing. Mechanical keyboards are not for everyone, but when you are looking for a keyboard I recommend you at least look for an ergonomic keyboard to prevent yourself from getting carpal tunnel later down the road. For me personally I got the <a href="https://ergodox-ez.com/">Ergodox EZ</a> keyboard with brown mechanical switches. This is going to be a really hard keyboard for anybody to start using because you will feel like a novice trying to type on it for the first two weeks. After spending a considerable amount of time typing on it and configuring it to work with my flow. I can type just as fast, if not faster than I did on a normal keyboard and I’m doing it in an ergonomic way!</p>
<p>Below is a video to from ergo dox about what its all about in a nut shell.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/hkdAQvBFNkM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
<p>If you’re interested to see what my configuration is personally for the keyboard, check out my configurations on the <a href="https://configure.ergodox-ez.com/ergodox-ez/search?q=plusmobileapps&legacy=false">ergo dox ez configurator</a> which I have tried to optimize for a Mac/Windows setup.</p>
<h2 id="monitor">Monitor</h2>
<p>For a monitor, I recommend getting either dual monitors or one big curved monitor to give yourself the real estate for increased multitasking. I myself prefer the big curved monitor from an ergonomic standpoint because its equivalent to having one and a half monitors which helps reduce the strain on my neck from turning to far looking at a dual monitor setup. Some people prefer the dual monitor setup though, so wouldn’t rule that out for yourself if that is what works for you.</p>
<p>I personally got the <a href="https://www.lg.com/us/monitors/lg-34UC98-W-ultrawide-monitor">34’’ Class 21:9 UltraWide® WQHD IPS Thunderbolt™ Curved LED Monitor</a> which may have been a bit of overkill, but I also game and wanted a monitor that would have a fast response time for that. If you are looking for another curved monitor that is just for work, there are plenty of other great options out there that are more affordable.</p>
<h2 id="chair">Chair</h2>
<p>The chair is one of the most important things to invest in since its where you’re sitting most of the day. So make sure whatever you choose to get is comfortable and ergonomic to sit in all day. I personally got the <a href="https://secretlab.co/collections/titan-series">Secret Lab Titan Gaming Chair</a> and may have even splurged for the limited edition Batman version of the chair. Very happy with the quality and comfort from the Secret lab chair!</p>Andrew Steinmetzandrew@plusmobileappsOne aspect of a programmer’s work that I think can get overlooked is the actual physical workspace where he or she works. Over the years I have invested a lot of time and effort into creating a home office that is ergonomic and promotes a workflow that works for me. By no means am I trying to tell everyone that…My Mac Developing Environment2020-01-26T00:00:00+00:002020-01-26T00:00:00+00:00https://plusmobileapps.com/2020/01/26/mac-dev-environment<p><img src="/assets/images/my-mac-environment.png" alt="" /></p>
<p>A list of all the different programs, packages, and tips for how I configure my Mac for development.</p>
<!--more-->
<h2 id="installed-from-terminal">Installed From Terminal</h2>
<p><a href="https://brew.sh/">Home Brew</a> - helps you install packages from the command line more easily similar to linux’s package manager</p>
<p><a href="https://gist.github.com/derhuerst/1b15ff4652a867391f03#file-mac-md">Git</a> - version control system</p>
<p><a href="https://www.iterm2.com/">iTerm2</a> - terminal replacement</p>
<p><a href="https://github.com/robbyrussell/oh-my-zsh">Oh My ZShell</a> - customize the terminal</p>
<ul>
<li>I use the <code class="language-plaintext highlighter-rouge">agnoster</code> theme. To change open up <code class="language-plaintext highlighter-rouge">~/.zshrc</code> file and change the following line</li>
</ul>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ZSH_THEME</span><span class="o">=</span><span class="s2">"agnoster"</span>
</code></pre></div></div>
<ul>
<li>If the theme is not rendering properly in iTerm, then install <a href="https://github.com/powerline/fonts">Powerline fonts</a>. Copy/paste the following to install. Then in iTerm preferences, check the option to <code class="language-plaintext highlighter-rouge">Use a differnt font for non-ASCII text</code> and switch the font to <code class="language-plaintext highlighter-rouge">Mesio LG L for powerline</code>. <a href="https://github.com/ohmyzsh/ohmyzsh/issues/1906#issuecomment-252443982">Screen shot</a></li>
</ul>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># clone</span>
git clone https://github.com/powerline/fonts.git <span class="nt">--depth</span><span class="o">=</span>1
<span class="c"># install</span>
<span class="nb">cd </span>fonts
./install.sh
<span class="c"># clean-up a bit</span>
<span class="nb">cd</span> ..
<span class="nb">rm</span> <span class="nt">-rf</span> fonts
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins">Oh My ZShell Plugins</a> that I use - <code class="language-plaintext highlighter-rouge">plugins=(git adb vscode)</code>
<ul>
<li><a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/adb">adb</a> - Android Debug Bridge autocomplete plugin</li>
<li><a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git">git</a> - aliases and autocomplete for git</li>
<li><a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/vscode">vscode</a> - aliases and autocomplete for visual studio code editor</li>
</ul>
</li>
</ul>
<h2 id="downloadable-applications">Downloadable Applications</h2>
<p><a href="https://code.visualstudio.com/">Visual Studio Code</a> - text editor and markdown editor</p>
<ul>
<li>to open files from the command line follow these <a href="https://code.visualstudio.com/docs/setup/mac">instructions</a></li>
<li><code class="language-plaintext highlighter-rouge">git config --global core.editor "code --wait"</code> - to configure git as the default editor</li>
</ul>
<p><a href="https://www.sourcetreeapp.com/">Sourcetree</a> - version control GUI for Git repositories</p>
<p><a href="https://www.spectacleapp.com/">Spectacle</a> - window control management tool for Mac</p>
<p><a href="https://developer.android.com/studio/">Android Studio</a> - IDE for developing Android applications</p>
<p><a href="https://www.jetbrains.com/idea/">IntelliJ Idea</a> - IDE that Android Studio was based off and I use for developing any Kotlin Multiplatform apps</p>
<p><a href="https://github.com/mortenjust/droptogif">Drop to GIF</a> - easy tool to convert videos to GIFs that I use for adding GIFs to pull requests</p>
<p><a href="https://postgresapp.com/">Postgres.app</a> - mac app that makes it dead simple to start up a <a href="https://www.postgresql.org/">PostgreSQL</a> server</p>
<h2 id="bash-profile">Bash Profile</h2>
<p>Since I use <a href="https://github.com/robbyrussell/oh-my-zsh">Oh My ZShell</a>, my bash profile is sourced from <code class="language-plaintext highlighter-rouge">.zshrc</code> file in my home directory as opposed to <code class="language-plaintext highlighter-rouge">.bash_profile</code>. My bash profile consists of a bunch of git aliases and helper functions for dealing with the Android SDK. For some Android specific bash profile functions & aliases, check out <a href="https://plusmobileapps.com/2019/03/05/android-terminal-tricks.html">Android Bash Profile and Terminal Tricks</a></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">edit_profile</span><span class="o">=</span><span class="s1">'code ~/.zshrc'</span>
<span class="nb">alias </span><span class="nv">source_profile</span><span class="o">=</span><span class="s1">'source ~/.zshrc'</span>
<span class="nb">alias </span><span class="nv">gs</span><span class="o">=</span><span class="s1">'git status'</span>
<span class="nb">alias </span><span class="nv">branches</span><span class="o">=</span><span class="s1">'git branch | cat'</span>
<span class="nb">alias </span><span class="nv">rbranches</span><span class="o">=</span><span class="s1">'git branch -r | cat'</span>
<span class="nb">alias </span><span class="nv">gamend</span><span class="o">=</span><span class="s1">'git commit --amend'</span>
<span class="c"># remove the user name and machine from the start of the terminal prompt</span>
<span class="c"># should only readout the current file name. </span>
<span class="nb">export </span><span class="nv">DEFAULT_USER</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">whoami</span><span class="si">)</span><span class="s2">"</span>
<span class="c"># when ran from the root of a git repo, will take an argument for the branch name</span>
<span class="c"># will then check if your current work station is clean, if not you can type "stash" to stash them</span>
<span class="c"># or will do nothing and not checkout the branch</span>
<span class="c"># After finished reviewing code, hit enter and it will remove the code reviewed branch from your local machine </span>
<span class="c"># and checkout your existing branch</span>
code_review<span class="o">()</span> <span class="o">{</span>
<span class="nv">branch</span><span class="o">=</span><span class="si">$(</span>git branch | <span class="nb">sed</span> <span class="nt">-n</span> <span class="nt">-e</span> <span class="s1">'s/^\* \(.*\)/\1/p'</span><span class="si">)</span>
git diff-index <span class="nt">--quiet</span> HEAD
<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="o">=</span> 1 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s2">"Branch: </span><span class="nv">$branch</span><span class="s2"> is dirty, if you would like to stash your changes type stash"</span>
<span class="nb">read </span>input
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$input</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"stash"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>git stash
git fetch
git checkout <span class="nv">$1</span>
<span class="nb">echo</span> <span class="s2">"Hit enter when you are done reviewing this branch"</span>
<span class="nb">read </span>userInput
git reset <span class="nt">--hard</span>
git checkout <span class="nv">$branch</span>
git branch <span class="nt">-d</span> <span class="nv">$1</span>
git stash pop
<span class="k">else
</span><span class="nb">echo</span> <span class="s2">"Cool, nothing happened"</span>
<span class="k">fi</span><span class="p">;</span>
<span class="k">else
</span>git fetch
git checkout <span class="nv">$1</span>
<span class="nb">echo</span> <span class="s2">"Hit enter when you are done reviewing this branch"</span>
<span class="nb">read </span>userInput
git reset <span class="nt">--hard</span>
git checkout <span class="nv">$branch</span>
git branch <span class="nt">-d</span> <span class="nv">$1</span>
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="change-location-of-where-screenshots-get-saved">Change Location of Where Screenshots Get Saved</h2>
<p>Open up a terminal and enter the following two commands.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.screencapture location <folder location>
killall SystemUIServer
</code></pre></div></div>
<p>For me, I typically save any screenshots in <code class="language-plaintext highlighter-rouge">~/Pictures/screenshots</code>.</p>
<p>Then if you would like even quicker access to your screenshots, I will click and drag that folder to the bottom right section of dock next to Downloads.</p>
<p>Then whenever you take a screen shot, you will see it show up in your bottom toolbar.</p>
<p>Just a reminder to take a screen shot of the whole screen, you can use the following command.</p>
<p><img src="/assets/images/screen-cap-whole-screen.png" alt="full screen capture" /></p>
<p>Then to take a screen shot of just a portion of the screen you can use:</p>
<p><img src="/assets/images/screen-cap-part-screen.png" alt="portion of the screen capture" /></p>Andrew Steinmetzandrew@plusmobileappsA list of all the different programs, packages, and tips for how I configure my Mac for development.Markdown Cheat Sheet2020-01-20T00:00:00+00:002020-01-20T00:00:00+00:00https://plusmobileapps.com/2020/01/20/markdown<p><img src="/assets/images/markdown-guide.jpg" alt="" /></p>
<p>Markdown is a shorthand syntax for writing <strong>highly</strong> <em>stylized</em> text. It can be a great tool when you are taking notes, documenting code, or even writing a site like this one.</p>
<p>Below you will find a cheat sheat of all the different types of markdown text available for most markdown flavors, as well as some extensions utilized in this site.</p>
<!--more-->
<h2 id="headerstitling">Headers/Titling</h2>
<p>To do headers and subtitles, use a <code class="language-plaintext highlighter-rouge">#</code>. One <code class="language-plaintext highlighter-rouge">#</code> is the largest, and adding another <code class="language-plaintext highlighter-rouge">#</code> continues to make the text smaller.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## H2 Header
### H3 Header
#### H4 Header
</code></pre></div></div>
<h2 id="h2-header">H2 Header</h2>
<h3 id="h3-header">H3 Header</h3>
<h4 id="h4-header">H4 Header</h4>
<h2 id="text-styling">Text Styling</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*Italicized text has its text surrounded by single stars*
**Bold text has its text surrounded by double stars**
</code></pre></div></div>
<p><em>Italicized text has its text surrounded by single stars</em></p>
<p><strong>Bold text has its text surrounded by double stars</strong></p>
<h2 id="bullets-and-numbers">Bullets And Numbers</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* One bullet
* indented bullets have two spaces from its parent
* second bullet
1. Numbering can be done as well
2. Just by using numers
3. Which will indent the numbers
</code></pre></div></div>
<ul>
<li>One bullet
<ul>
<li>indented bullets have two spaces from its parent</li>
</ul>
</li>
<li>second bullet</li>
</ul>
<ol>
<li>Numbering can be done as well</li>
<li>Just by using numers</li>
<li>Which will indent the numbers</li>
</ol>
<h2 id="block-quotes">Block Quotes</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> Block quotes just use a single caret to create an indented paragraph.
</code></pre></div></div>
<blockquote>
<p>Block quotes just use a single caret to create an indented paragraph.</p>
</blockquote>
<h2 id="coding">Coding</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`SomeClass` - uses back ticks around classes and functions to indicate this is the exact name in code.
```kotlin
// Code comments
val foo = "bar"
println(foo)
```
</code></pre></div></div>
<p>Below are some sample styles for markdown code highlighting to be placed after the first three backticks.</p>
<ul>
<li>kotlin</li>
<li>json</li>
<li>bash</li>
<li>java</li>
<li>javascript</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">SomeClass</code> - uses back ticks around classes and functions to indicate this is the exact name in code.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Code comments</span>
<span class="kd">val</span> <span class="py">foo</span> <span class="p">=</span> <span class="s">"bar"</span>
<span class="nf">println</span><span class="p">(</span><span class="n">foo</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="hyperlinking">Hyperlinking</h2>
<p>If you want to create blue text that hyperlinks to a website like <a href="https://www.google.com">Google</a>, Surround the text to be shown in blue around brackets, and parantheses around the actual website link just like: <code class="language-plaintext highlighter-rouge">[Google](www.google.com)</code></p>
<h2 id="images">Images</h2>
<p>Images supported in markdown are png, jpg, and gif’s.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>![Sample Screenshot description](/assets/images/sample.gif)
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">![The description that displays to the user when hovered over](relative path to image)</code></p>
<p><img src="/assets/images/sample.gif" alt="Sample Screenshot description" /></p>
<h2 id="tables">Tables</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>| 1 | 2 | 3 |
|---|---|---|
| Fizz | Buzz | fizz |
| fiz | buzz | buzz |
</code></pre></div></div>
<table>
<thead>
<tr>
<th>1</th>
<th>2</th>
<th>3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fizz</td>
<td>Buzz</td>
<td>fizz</td>
</tr>
<tr>
<td>fiz</td>
<td>buzz</td>
<td>buzz</td>
</tr>
</tbody>
</table>
<h2 id="jekyll-text-theme-markdown-enhancements">Jekyll Text Theme Markdown Enhancements</h2>
<h2 id="material-mkdocs-markdown">Material Mkdocs Markdown</h2>
<p>This site is generated by <a href="https://tianqi.name/jekyll-TeXt-theme/">Jekyll Text Theme</a> which has a little extra flavor of markdown to add even more styles that are missing in markdown.</p>
<ul>
<li><a href="https://tianqi.name/jekyll-TeXt-theme/docs/en/markdown-enhancements">Jekyll Text Theme - Markdown Enhancements</a></li>
</ul>Andrew Steinmetzandrew@plusmobileappsMarkdown is a shorthand syntax for writing highly stylized text. It can be a great tool when you are taking notes, documenting code, or even writing a site like this one. Below you will find a cheat sheat of all the different types of markdown text available for most markdown flavors, as well as some extensions utilized in this site.Learning Some iOS as a Kotlin Developer2019-11-01T00:00:00+00:002019-11-01T00:00:00+00:00https://plusmobileapps.com/2019/11/01/ios-as-a-kotlin-dev<p>I would consider myself a professional Kotlin developer during the day, and this is just a collection of some notes I have taken away from the little I learned about iOS when I tried to implement an iOS app called <a href="https://github.com/plusmobileapps/koddit">Koddit</a> with Kotlin Multiplatform.</p>
<!--more-->
<h2 id="immutability-and-mutability">Immutability and Mutability</h2>
<ul>
<li><strong>Immutable</strong> - the value assigned to a variable is constant and cannot be changed or <em>mutated</em></li>
<li><strong>Mutable</strong> - value assigned to a variable can be changed by reassigning another value to it</li>
</ul>
<table>
<thead>
<tr>
<th>Type</th>
<th>Swift</th>
<th>Kotlin</th>
</tr>
</thead>
<tbody>
<tr>
<td>Immutable</td>
<td><code class="language-plaintext highlighter-rouge">let</code></td>
<td><code class="language-plaintext highlighter-rouge">val</code></td>
</tr>
<tr>
<td>Mutable</td>
<td><code class="language-plaintext highlighter-rouge">var</code></td>
<td><code class="language-plaintext highlighter-rouge">var</code></td>
</tr>
</tbody>
</table>
<p>Best practice is to always make variables immutable by default and convert to mutable variables if you find the value needing to change. Immutable variables are very useful when working from multiple threads as it will ensure values don’t switch out from under you as another thread could have altered that value.</p>
<h1 id="xcode-tips">XCode Tips</h1>
<h2 id="storyboard">Storyboard</h2>
<h3 id="keyboard-shortcuts">Keyboard Shortcuts</h3>
<table>
<thead>
<tr>
<th>Action</th>
<th>Keys</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Access libraries</td>
<td>Command + Shift + L</td>
<td>open the libraries popup to easily access common views such as labels and buttons</td>
</tr>
<tr>
<td>Open file in new editor</td>
<td>Option + Shift</td>
<td>move the cursor to the right side of current editor to open in new editor. Once two editors are open, you can move the cursor into which ever windo to open the file in that window.</td>
</tr>
</tbody>
</table>
<h3 id="outlets-and-actions">Outlets and Actions</h3>
<p><strong>Outlets</strong> - reference to view in code to make dynamic changes to at run time from code</p>
<p><strong>Action</strong> - reference to a piece of code that will run when the user interacts with a control</p>
<p>To connect a storyboard view to a specific part of code, open the <code class="language-plaintext highlighter-rouge">ViewController</code> file in a separate editor. Then <code class="language-plaintext highlighter-rouge">control + click and drag</code> to the <code class="language-plaintext highlighter-rouge">ViewController</code>. Then in the submenu that pops up, you should have the option to select the type of connection you want to make (action or outlet). You can also assign it a label to be generated in code. Note there is a little circle that appears next to the connection, when it is filled this indicates that the variable is actually referencing something from the storyboard. If you delete the view, you will see that circle no longer be filled indicating that it is not referencing anything.</p>
<p><img src="/assets/images/actions-outlets.gif" alt="Connecting actions and outlets" /></p>Andrew Steinmetzandrew@plusmobileappsI would consider myself a professional Kotlin developer during the day, and this is just a collection of some notes I have taken away from the little I learned about iOS when I tried to implement an iOS app called Koddit with Kotlin Multiplatform.