<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Hossain's Thoughts]]></title><description><![CDATA[Hossain's Thoughts]]></description><link>https://blog.hossain.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 10:58:11 GMT</lastBuildDate><atom:link href="https://blog.hossain.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Deploy GitHub's Spark App to Cloudflare Workers]]></title><description><![CDATA[GitHub's Spark is a fantastic starting point for building modern React applications that uses spark-template as starting point. Once you have built the app, GitHub does allow you to publish site, howe]]></description><link>https://blog.hossain.dev/how-to-deploy-githubs-spark-app-to-cloudflare-workers</link><guid isPermaLink="true">https://blog.hossain.dev/how-to-deploy-githubs-spark-app-to-cloudflare-workers</guid><category><![CDATA[github-spark]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Cloudflare Workers]]></category><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sun, 28 Sep 2025 12:33:52 GMT</pubDate><content:encoded><![CDATA[<p>GitHub's <a href="https://github.com/features/spark">Spark</a> is a fantastic starting point for building modern React applications that uses <a href="https://github.com/github/spark-template">spark-template</a> as starting point. Once you have built the app, GitHub does allow you to publish site, however that requires users to have GitHub account to visit the site.</p>
<p>In this guide, we'll walk through the process of adapting the spark-template to deploy seamlessly to Cloudflare Workers using the modern ASSETS binding approach.</p>
<h2>Why Cloudflare Workers?</h2>
<p>Cloudflare Workers offers several compelling advantages for deploying React SPAs:</p>
<p>• <strong>Global CDN</strong>: Your app is served from 330+ locations worldwide<br />• <strong>Zero-config static hosting</strong>: No server configuration needed<br />• <strong>SPA routing support</strong>: Client-side routes work seamlessly<br />• <strong>Cost-effective</strong>: Free tier includes 100,000 requests/day<br />• <strong>Instant cold starts</strong>: No server spin-up time<br />• <strong>Built-in security</strong>: Automatic HTTPS, DDoS protection, and rate limiting</p>
<h2>Prerequisites</h2>
<p>Before we begin, make sure you have:</p>
<ol>
<li><p><strong>Cloudflare Account</strong>: Sign up at <a href="http://cloudflare.com">cloudflare.com</a></p>
</li>
<li><p><strong>Spark App</strong>: The Spark app must be published to your GitHub repository.</p>
</li>
</ol>
<h2>Step-by-Step Implementation</h2>
<p>For this example, I have created a Spark app called JSON to CSV Converter. I will use that to show as example on how to configure it. Update the key information of the project where applicable.</p>
<h3>Prepare Project for Cloudflare Workers</h3>
<p><strong>Create the Cloudflare Worker Script</strong></p>
<p>Create a new file <code>src/_worker.js</code> with the following content:</p>
<pre><code class="language-javascript">/**
 * Cloudflare Worker for Spark Template App
 * Simple ASSETS binding approach for static site hosting
 */
export default {
  async fetch(request, env, ctx) {
    // Serve static assets using the ASSETS binding
    return env.ASSETS.fetch(request);
  },
};
</code></pre>
<p>This minimal worker leverages Cloudflare's ASSETS binding to serve your static files without any custom logic.</p>
<p><strong>Create Assets Ignore File</strong></p>
<p>Create <code>src/.assetsignore</code> to exclude the worker script from being served as a static asset:</p>
<pre><code class="language-plaintext">_worker.js
</code></pre>
<p><strong>Create worker config</strong></p>
<p>Create a <code>wrangler.toml</code> file in your project root:</p>
<pre><code class="language-toml">name = "my-spark-app"
main = "dist/_worker.js"
compatibility_date = "2025-08-29"
workers_dev = true

[assets]
directory = "./dist"
binding = "ASSETS"
html_handling = "auto-trailing-slash"
not_found_handling = "single-page-application"

[build]
command = "npm run build"
</code></pre>
<p>Key configuration options:</p>
<ul>
<li><p><code>not_found_handling = "single-page-application"</code>: Enables client-side routing</p>
</li>
<li><p><code>html_handling = "auto-trailing-slash"</code>: Handles clean URLs</p>
</li>
<li><p><code>directory = "./dist"</code>: Points to your build output</p>
</li>
<li><p><code>name = "my-spark-app"</code>: This will be the app id you use when creating worker in cloudflare</p>
</li>
</ul>
<p><strong>Update package.json Scripts</strong></p>
<p>Add the deployment script to your <code>package.json</code>: Add ‘copy-worker’ step and update ‘build’ step.</p>
<pre><code class="language-json">{
  "scripts": {
    "copy-worker": "cp src/_worker.js dist/_worker.js &amp;&amp; cp src/.assetsignore dist/.assetsignore",
    "build": "tsc -b --noCheck &amp;&amp; vite build &amp;&amp; npm run copy-worker",
  }
}
</code></pre>
<p>The key addition is <code>"copy-worker"</code> step that copies necessary files to <code>dist/</code> directory. Then, the step is used in the <code>"build"</code> step.</p>
<p><strong>Update Vite Configuration (if needed)</strong></p>
<p>If you encounter build issue in cloudflare, update following line from:</p>
<pre><code class="language-typescript">const projectRoot = process.env.PROJECT_ROOT || import.meta.dirname
</code></pre>
<p>to</p>
<pre><code class="language-typescript">const projectRoot = process.env.PROJECT_ROOT || process.cwd()
</code></pre>
<h2>File Structure After Implementation</h2>
<p>Your project structure should look like this:</p>
<pre><code class="language-plaintext">spark-template/
├── src/
│   ├── _worker.js          # Cloudflare Worker script
│   ├── .assetsignore       # Excludes worker from static assets
│   └── [your app files]    # React components and assets
├── dist/ (after build)
│   ├── _worker.js          # Copied worker script
│   ├── .assetsignore       # Copied asset exclusions
│   ├── index.html          # Built app entry point
│   └── assets/             # Built app assets
├── wrangler.toml           # Cloudflare Workers configuration
├── vite.config.ts          # Updated with minor fix
└── package.json            # Updated with 'copy-worker' and 'build' script
</code></pre>
<h2>Cloudflare Workers Deployment</h2>
<p>Once you've made all the changes, creating cloudflare workers is easy. First, ensure your GitHub account is connected so that when importing project you can see your repository.</p>
<h3>Importing Repository</h3>
<p>First go to workers section and click on “Create application”</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759061937050/055ad2d1-8cb2-42cb-b5a3-429205fd87c0.png" alt="" style="display:block;margin:0 auto" />

<p>You should be presented with following screen, where you should select “<strong>Import a repository</strong>”</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759061739224/1588b893-be1a-4e36-b8f9-00c599f4552d.png" alt="" style="display:block;margin:0 auto" />

<p>Since your GitHub is connected, it should be able to show the Spark project in the dropdown list. Pick the Spark project and continue.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759062040111/f1ff0fa2-7bb7-4bde-8e16-62708abaeabc.png" alt="" style="display:block;margin:0 auto" />

<p>On the “Set up your application” leave the default except the “Project name” that should match what you used in the <code>wrangler.toml</code> file above.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759062070590/a6991393-0847-4e76-a682-909eddebaae8.png" alt="" style="display:block;margin:0 auto" />

<p>Once you click on “Create and deploy” it should checkout the code and start deploying to Cloudflare network.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759062288004/11abb91c-27a4-45b4-9b62-0ff4d778fe87.png" alt="" style="display:block;margin:0 auto" />

<p>Take a look into build log, it ideally should succeed if everything went well. If it hasn’t, you can still continue to project screen and click on “<strong>Deployments</strong>” tab to get back to build log and fix any outstanding issue.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759062357525/b7bf6e59-c459-485e-8a07-37850cb511e7.png" alt="" style="display:block;margin:0 auto" />

<p>Once build is complete successfully, clicking on the external link button on the top should take you to the site. Or you can go to “<strong>Settings</strong>” tab and get the domain from “<strong>Domains &amp; Routes</strong>” section.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759062587229/ee1d9837-6f6b-4e7b-8849-8653a40d37fe.png" alt="" style="display:block;margin:0 auto" />

<p>✓ Thats it, you got your Spark app up and running on Cloudflare's edge network. If you have domain hosted with Cloudflare you can also use that as custom domain or sub-domain for the app.</p>
<h2>Troubleshooting</h2>
<h3>Build Issues</h3>
<pre><code class="language-bash"># Clear and rebuild
rm -rf node_modules dist
npm install
npm run build
</code></pre>
]]></content:encoded></item><item><title><![CDATA[‘Vibe Coding’ to ‘Blind Coding’ 😎]]></title><description><![CDATA[TL;DR: I let AI write a JSON5 parser for me using just test cases and vibes. No deep knowledge, just TDD and Agentic AIs. Shockingly, it worked!


Can you be irresponsibly responsible? ^_^

That was t]]></description><link>https://blog.hossain.dev/vibe-coding-to-blind-coding-986f8f0b7b09</link><guid isPermaLink="true">https://blog.hossain.dev/vibe-coding-to-blind-coding-986f8f0b7b09</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Wed, 11 Jun 2025 04:37:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751406526178/3067e87b-8e78-42a9-8086-3664ebead80c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR:</strong> I let AI write a JSON5 parser for me using just test cases and vibes. No deep knowledge, just TDD and Agentic AIs. Shockingly, it worked!</p>
</blockquote>
<blockquote>
<p>Can you be irresponsibly responsible? ^_^</p>
</blockquote>
<p>That was the experiment I wanted to do with a plethora of Agentic AI available for code assist.</p>
<p>I am sure I don’t need to define <a href="https://en.wikipedia.org/wiki/Vibe_coding">Vibe Coding</a> to you — but, what is “Blind Coding” you ask!? I think you have guessed it by now — It’s about almost blindly accepting AI-generated code.</p>
<p>Sounds very irresponsible, right? But there is a twist and that is part of my experiment 🧪</p>
<ul>
<li>I wanted to explore if I could do TDD (<em>Test Driven Development</em>) with AI on a topic with <em><strong>minimal understanding</strong></em> of what the suggested code does.</li>
<li>However, I would have a very good understanding of expected <em><strong>input &amp; output</strong></em> such that I can have well-defined test cases that I can feed AI Agents to work with.</li>
</ul>
<h4>Test Subject — Build a JSON5 Parser</h4>
<p>For this experiment I chose <a href="https://json5.org/">JSON5</a> that I recently came across. This is an extension of JSON with the option to add comment and more, making it suitable for configuration definitions (like YAML).</p>
<p>Since <code>**JSON5**</code> has a well-defined grammar and specification, it would be easy for me to point to those and <strong>build a library</strong> that parses JSON5 files using Kotlin language.</p>
<h4>Results First 📊</h4>
<p>Before I dive deep into the ‘Blind Coding’ strategy, let me reveal what was achieved first using LLM based Agentic AI 🤖</p>
<ul>
<li>Fully functional <code>JSON5</code> de/serializer of Kotlin Data classes using <code>kotlinx.serialization</code></li>
<li>Fully functional <code>JSON5</code> parser that complies with specification and provides parsed data</li>
</ul>
<p>Here is <a href="https://github.com/hossain-khan/json5-kotlin/tree/main/benchmark">benchmark</a> of generated library vs two relevant contender 💪</p>
<ol>
<li>JSON5-AI — AI generated library built with ‘Blind Coding’</li>
<li><a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a> — JetBrains library used <em>only</em> for JSON serialization and deserialization. Which has less complexity compared to JSON5.</li>
<li>Syntaxerror’s <a href="https://github.com/Synt4xErr0r4/json5">JSON5 Java Library</a> — an actively maintained implementation of JSON5. Marked as ‘3P-External-JSON5’ in the chart below.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751406520061/2f392af4-27ef-4469-8687-e058fe2b54fb.png" alt="" /></p>
<p>Total processing time by library</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751406522221/490ef8e1-dd6e-41e8-b7d5-67ceb347c6e9.png" alt="" /></p>
<p>Total time spent for each test category</p>
<p>Regardless of the generated library being ~4x and ~1.5x less performant compared to kotlinx.serialization and Syntaxerror’s Java library respectively, the experiment is a ✅ success in my book! Because it’s functional and has very acceptable performance for initial phase.</p>
<p>If you are interested to look under the hood, take a look into <code>**lib**</code> module in this GitHub project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751406524542/3cc306f7-52b9-4360-b257-5056011f1580.png" alt="" /></p>
<p><a href="https://github.com/hossain-khan/json5-kotlin" title="https://github.com/hossain-khan/json5-kotlin"><strong>GitHub - hossain-khan/json5-kotlin: JSON5 implementation for Kotlin/JVM</strong><br />*JSON5 implementation for Kotlin/JVM. Built by Coding AI Agents.*github.com</a><a href="https://github.com/hossain-khan/json5-kotlin"></a></p>
<h4>Behind the Scenes</h4>
<p>Believe it or not, JSON5 has been around since 2012 and has all the implementation details and specification on their GitHub repos (<a href="https://github.com/json5">https://github.com/json5</a>).</p>
<p>All I had to do was import their JavaScript based implementation and specification and <em><strong>iteratively</strong></em> ask agents to build functionalities.</p>
<p>⏳ <strong>How long did it take to build the JSON5 library?</strong><br />About 4 days of weekend and after work sessions (roughly 10–12 hours)</p>
<p>😅 <strong>Was there challenges along the way?</strong><br />Yes, some of the parsing tests were failing and agent was repeatedly failing to fix it. I almost lost hope. Then GitHub Copilot Agent rescued me.</p>
<p>🤖 <strong>What are the Agents that were used to achieve this?</strong><br />- Claude Sonnet 3.7 (Agent Mode — IDE integration)<br />- GPT 4.1 (Agent Mode — IDE integration)<br />- Google Jules (async agent)<br />- GitHub Copilot Agent (async agent — used for majority of the project)</p>
<p>🎉 <strong>So, what’s the verdict?</strong><br />I had absolute blast just watching agents do magical stuff that I couldn’t have done by myself. Even the benchmarking solution was build by Agent. <br />Seeing the benchmark result also gave me hope and sense of accomplishment. Sure, I might not be able to build the next Dagger, or OkHttp or anything of that sort, however anything simple should clearly be within reach with the limited time we have in our lives. That is a huge win for me! 🏆</p>
<p>Now, on to something more useful! 🤓 Feel free to reach out or ask question. Cheers! ✌🏽</p>
]]></content:encoded></item><item><title><![CDATA[Using AI to build modern Android apps — are we there yet?]]></title><description><![CDATA[Building is more fun with AI 🤖
I’ve had my GitHub Copilot subscription for over a year now, but I haven’t leveraged the service until very recently I decided to build some tools and Android apps usin]]></description><link>https://blog.hossain.dev/using-ai-to-build-modern-android-apps-are-we-there-yet-4c9b4455cd88</link><guid isPermaLink="true">https://blog.hossain.dev/using-ai-to-build-modern-android-apps-are-we-there-yet-4c9b4455cd88</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Tue, 18 Feb 2025 13:55:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751406529894/81627b9d-d330-4cf2-8595-ac4ee156286c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building is more fun with AI 🤖</p>
<p>I’ve had my GitHub Copilot subscription for over a year now, but I haven’t leveraged the service until very recently I decided to build some tools and Android apps using it.</p>
<p>I have extensively used ChatGPT 4o &amp; o1, Google Gemini, and Claude Sonnet to build an app related to weather even though I am very aware of the following phrase:</p>
<blockquote>
<p>“If all you have is a hammer, everything looks like a nail.”</p>
</blockquote>
<h4>The Rationale Behind the App</h4>
<p>You get many amazing things living in Canada, one of them is snow in winter. After moving to a home, I needed a way for me to be better prepared for snow days where I have to clean my driveway and potentially neighbour’s if the snowblower has enough charge.</p>
<p>So, I saw a “nail” that I can hammer, I built a single purpose Android app that notifies me if there will be enough snow, a threshold set by me. This allows me to make sure I charge the snowblower batteries and park the cars inside the garage. Profit!</p>
<p>Sure, there are some outstanding weather apps including Google’s own that can provide similar info, however the tinkering engineer wants to use the hammer! 😂</p>
<h4>The AI Code Assist Experience</h4>
<p>It’s been absolutely fun working with AI to come up with solutions, in this case the Compose UI Screens, Presenters, and unit tests for them. From my experience, the LLM provided solution are <strong>80–98% correct</strong> and can just be copy pasted with some imports to fix. Sometimes I do have to refactor or move things around, but it gets me 80% there and I just finish the last 20% to make a finished app screen.</p>
<p>I feel like, this will be an essential tool for all kinds of engineers to reduce the cognitive load and iterate on the products they want to build.</p>
<h4>Is the AI/LLM Ready to Print Out Full App?</h4>
<p>Short answer, NO <em>(based on my experience)</em>.</p>
<p>I have encountered some YouTube videos where they showcase building beautiful iOS apps using LLMs. It’s possible that I was not a good prompt engineer or GitHub Copilot is not the right product to build fully functional app.</p>
<p>Another limitation I noticed is, when asked to build full app, the LLM is likely limited by the length of response (tokens), so it responds in shorter chunks for each component of the app and does not provide fully detailed implementation. When asked in smaller chunks with specific steps, it can provide detailed code.</p>
<p>It might get there soon enough. Or, maybe if we use multi-modal LLM and provide fully designed mocks of the app, it will be able to build it.</p>
<p>Regardless, I am very happy to use LLM and learn from it through the process of building small apps.</p>
<h4>The Learning Opportunity</h4>
<p>One of the best part of leveraging AI and looking at the response is that you get to learn new function, tools and so on.</p>
<p>Along the way building the app, I got to 🧑‍💻 exercise my knowledge, learn and grow. Here are some of the cool stuff I have done while building the app</p>
<ul>
<li>Extensively leverage AI powered code assist, namely GitHub Copilot, ChatGPT, Google Gemini 🤖. I think more than 50% of the code is done by AI.</li>
<li>Learn ⚡ <a href="https://slackhq.github.io/circuit/">Circuit</a>, a modern Android app architecture from Slack (that’s what we also use at Slack)</li>
<li>Use Jetpack Room database for app, and also the SQLite (Kotlin Multiplatform) library for bundling city database used in the app</li>
<li>Jetpack Compose for building modern UI that follows Material 3 guidelines with dark and light mode support</li>
<li>Go through lengthy Google Play publishing process 😅</li>
<li>And all the other usual suspects like Retrofit, OkHttp, Dagger Anvil, Moshi, Firebase and more.</li>
</ul>
<p>Here is the app that is available on GitHub</p>
<p><a href="https://github.com/hossain-khan/android-weather-alert" title="https://github.com/hossain-khan/android-weather-alert"><strong>GitHub - hossain-khan/android-weather-alert: A simple app to provide configured weather alert so…</strong><br /><em>A simple app to provide configured weather alert so that you are ready for next hour or day!</em> github.com</a><a href="https://github.com/hossain-khan/android-weather-alert"></a></p>
<p>It’s also available on <a href="https://play.google.com/store/apps/details?id=dev.hossain.weatheralert&amp;pcampaignid=web_share">Google Play</a>, if the use-case mentioned talks to you :-)</p>
]]></content:encoded></item><item><title><![CDATA[Android remote logging to Airtable using Timber]]></title><description><![CDATA[Have you ever needed to save your Android Logcat logs for later analysis?
I recently built a service-based Android app, and I needed to monitor its activity to validate its correctness. For that I cou]]></description><link>https://blog.hossain.dev/android-remote-logging-to-airtable-using-timber</link><guid isPermaLink="true">https://blog.hossain.dev/android-remote-logging-to-airtable-using-timber</guid><category><![CDATA[Android]]></category><category><![CDATA[Timber]]></category><category><![CDATA[remote-logging]]></category><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Tue, 10 Sep 2024 04:03:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514845622/af1b44b0-fb0b-47b7-920d-12a35fb03741.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever needed to save your Android Logcat logs for later analysis?</p>
<p>I recently built a service-based Android app, and I needed to monitor its activity to validate its correctness. For that I could not sit in front of the computer monitor for 24+ hours, I needed a way to store the logs with app behavior to validate it’s performing as it should.</p>
<p>I leveraged the following 2 tools to accomplish this task</p>
<ul>
<li><p><a href="https://github.com/JakeWharton/timber"><strong>Timber</strong></a>: <em>A logger with a small, extensible API which provides utility on top of Android’s normal Log class.</em></p>
</li>
<li><p><a href="https://www.airtable.com/"><strong>Airtable</strong></a>: <em>Airtable is a hybrid solution that mixes the intuitiveness of spreadsheets and the power and functionalities of databases.</em></p>
</li>
</ul>
<blockquote>
<p>Airtable service can be replaced by any other similar service that allows REST API to add records that can be viewed online.</p>
</blockquote>
<h4>Why Airtable?</h4>
<ul>
<li><p>It provides an easy interface to view data in a spreadsheet-like experience.</p>
</li>
<li><p>It has REST API to easily add data from Android app.</p>
</li>
</ul>
<p>If you are not familiar with the amazing Android Timber library, I suggest you spend a few minutes reading the <a href="https://github.com/JakeWharton/timber/blob/trunk/README.md">documentation</a>. We will be building an <code>AirtableLoggingTree</code> that sends all the logs logged via Timber to the Airtable spreadsheet/database using their REST API.</p>
<p>Here are some pseudo task we need to accomplish this</p>
<ol>
<li><p>Save all the logs to a queue to process and send to REST API</p>
</li>
<li><p>Format the logs and send a batched request (to avoid rate limit)</p>
</li>
<li><p>Make the REST call based on API specification for <a href="https://airtable.com/developers/web/api/create-records">creating a record</a>.</p>
</li>
</ol>
<p>First, let’s build a Kotlin data class to capture some log info that should be sent to Airtable.</p>
<pre><code class="language-kotlin">data class LogMessage(
    val priority: Int,
    val tag: String?,
    val message: String,
    val throwable: Throwable?,
    val logTime: Instant = Instant.now(),
    val device: String = Build.MODEL,
)
</code></pre>
<p>Next, let’s create the <code>AirtableLoggingTree</code> that queues the logs and sends logs in batches using REST API via OkHttp client.</p>
<blockquote>
<p>💁Inline comments have been added where applicable.</p>
</blockquote>
<pre><code class="language-kotlin">class AirtableLoggingTree(
    // Create your token from https://airtable.com/create/tokens
    private val authToken: String,
    // Example: https://api.airtable.com/v0/appXXXXXXXXX/Table%20Name
    private val endpointUrl: String,
) : Timber.Tree() {
    private val client = OkHttpClient()
    private val logQueue = ConcurrentLinkedQueue&lt;LogMessage&gt;()
    private var flushJob: Job? = null

    companion object {
        /**
         * The API is limited to 5 requests per second per base.
         */
        private const val MAX_LOG_COUNT_PER_SECOND = 5

        /**
         * The API is limited to 10 records per request.
         * - https://airtable.com/developers/web/api/create-records
         */
        private const val MAX_RECORDS_PER_REQUEST = 10
    }

    init {
        startFlushJob()
    }

    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        // Adds the log to queue for batch processing
        logQueue.add(LogMessage(priority, tag, message, t))

        if (flushJob == null || flushJob?.isCancelled == true) {
            // If there is enough log, it will start sending them 
            startFlushJob()
        }
    }

    private fun startFlushJob() {
        flushJob =
            CoroutineScope(Dispatchers.IO).launch {
                while (isActive) {
                    flushLogs()
                    
                    // Wait long enough before sending next request 
                    // https://airtable.com/developers/web/api/rate-limits
                    delay(1_100L)
                }
            }
    }

    private fun createLogMessage(logs: List&lt;LogMessage&gt;): String? {
        if (logs.isEmpty()) {
            return null
        }

        // Builds the JSON payload structure with multiple messages based on API spec
        val records = JSONArray().apply { logs.forEach { put(it.toLogRecord()) } }
        return JSONObject().apply {
            put("records", records)
        }.toString()
    }

    private fun getMaximumAllowedLogs(): List&lt;LogMessage&gt; {
        val logs = mutableListOf&lt;LogMessage&gt;()
        while (logQueue.isNotEmpty() &amp;&amp; logs.size &lt; MAX_RECORDS_PER_REQUEST) {
            val log = logQueue.poll()
            if (log != null) {
                logs.add(log)
            }
        }
        return logs
    }

    private suspend fun flushLogs() {
        var sentLogCount = 0

        // Collects all the queued logs and sends them in batches
        // Based on rate-limit a total of 5x10 = 50 reconds can be sent per second
        while (sentLogCount &lt; MAX_LOG_COUNT_PER_SECOND) {
            val jsonPayload = createLogMessage(getMaximumAllowedLogs())
            if (jsonPayload != null) {
                sendLogToApi(jsonPayload)
                sentLogCount++

                // This delay is added to ensure the order of log is maintained.
                // However, there is no guarantee that the log will be sent in order.
                delay(100L)
            }
        }

        if (logQueue.isEmpty()) {
            flushJob?.cancel()
        }
    }

    // Finally, this is the OkHttp client that sends HTTP POST request to add those log records
    private fun sendLogToApi(logPayloadJson: String) {
        val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
        val body = logPayloadJson.toRequestBody(mediaType)
        val request =
            Request.Builder()
                .url(endpointUrl)
                .addHeader("Authorization", "Bearer $authToken")
                .post(body)
                .build()

        client.newCall(request).enqueue(
            object : okhttp3.Callback {
                override fun onFailure(
                    call: okhttp3.Call,
                    e: IOException,
                ) {
                    Timber.e("Failed to send log to API: ${e.localizedMessage}")
                }

                override fun onResponse(
                    call: okhttp3.Call,
                    response: okhttp3.Response,
                ) {
                    response.use { // This ensures the response body is closed
                        if (!response.isSuccessful) {
                            Timber.e(
                                "Log is rejected: HTTP code: ${response.code}, " +
                                    "message: \({response.message}, body: \){response.body?.string()}",
                            )
                        }
                    }
                }
            },
        )
    }
}
</code></pre>
<p>Everything is great except there is one piece of missing code that uses the API <code>LogMessage.toLogRecord()</code> in the <code>AirtableLoggingTree</code>. The snipped is shown below for clarity.</p>
<pre><code class="language-kotlin">val records = JSONArray().apply { logs.forEach { put(it.toLogRecord()) } }
</code></pre>
<p>This function implementation will be very specific to how you have setup your Airtable spreadsheet.</p>
<p>For my case, I have two columns for the table in the Airtable base:</p>
<ol>
<li><p><code>"Device"</code> — to store the device model. Single-line text.</p>
</li>
<li><p><code>"Log"</code> — to store the log message. Multiline text.</p>
</li>
</ol>
<pre><code class="language-kotlin">fun toLogRecord(): JSONObject {
    val logMessage =
        buildString {
            append("Priority: ${priority}\n")
            if (tag != null) append("Tag: $tag\n")
            append("Message: $message\n")
            if (throwable != null) append("Throwable: ${throwable.localizedMessage}")
            append("App Version: ${BuildConfig.VERSION_NAME}\n")
            append("Log Time: ${logTime}\n")
        }

    val fields =
        JSONObject().apply {
            put("Device", device)
            put("Log", logMessage)
        }
    return JSONObject().apply {
        put("fields", fields)
    }
}
</code></pre>
<p>You could essentially have multiple columns for each of the properties.</p>
<h4>Planting your <code>AirtableLoggingTree</code> 🌳</h4>
<p>In your app’s application class you can decide when to add the remote logging tree. Here is an example where remote logging is enabled regardless of <code>DEBUG</code> or <code>RELEASE</code> build.</p>
<pre><code class="language-kotlin">if (BuildConfig.DEBUG) {
    Timber.plant(Timber.DebugTree())
}

// Add your remote logging tree with configs
Timber.plant(
    AirtableLoggingTree(
        authToken = "yourAuthTokenXXXXXXXXXXXXXXXX",
        endpointUrl = "https://api.airtable.com/v0/appXXXXXXXXX/RemoteLogs",
    )
)
</code></pre>
<p>✅ And that’s it. You have a working Timber tree that can send all logcat logs to the Airtable base (spreadsheet) that adheres to their API rate limit.</p>
<p>Continue to log using Timber as usual, all logs will be also sent to Airtable</p>
<pre><code class="language-kotlin">Timber.tag("Cart").d("New item added to cart")

// Log will show in Airtable as follows (based on formatting we used in `toLogRecord()`)
// | Device | Log |
// | Pixel 8 | Priority: 3, Tag: Cart, Message: New item added to cart, App Version: v1.24, Log Time: 2024-09-10T05:28:04.113910Z
</code></pre>
<h4>See it in action</h4>
<p>You can take a look at the fully <a href="https://github.com/hossain-khan/android-keep-alive/blob/main/app/src/main/java/dev/hossain/keepalive/log/ApiLoggingTree.kt">implemented tree</a> in the following project</p>
<p><a href="https://github.com/hossain-khan/android-keep-alive">GitHub - hossain-khan/android-keep-alive</a></p>
<p>Here is a screenshot of how the log looks in the Airtable</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514843803/8a635ea4-c162-4bd4-8fc2-3c3402e0b833.png" alt="" />

<p>Demo of remotely logged messages in the Airtable</p>
]]></content:encoded></item><item><title><![CDATA[How to update a Docker Container using Portainer]]></title><description><![CDATA[ℹ️ This is part of the self-learning log as I explore Docker and Portainer.
Recently, I have been playing with Portainer — a Container Management system for Docker. Because of my curiosity, I constant]]></description><link>https://blog.hossain.dev/how-to-update-a-docker-container-using-portainer</link><guid isPermaLink="true">https://blog.hossain.dev/how-to-update-a-docker-container-using-portainer</guid><category><![CDATA[Portainer]]></category><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sat, 10 Aug 2024 20:30:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514840809/144a09f4-689b-489a-82e6-caac65763a4d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>ℹ️ This is part of the self-learning log as I explore Docker and Portainer.</em></p>
<p>Recently, I have been playing with <a href="https://www.portainer.io/">Portainer</a> — a Container Management system for Docker. Because of my curiosity, I constantly try different containers for different solutions and sometimes stick to some useful apps.</p>
<p>Once in a while, I keep seeing Docker containerized app notification that there is an update available. However, It was tricky for me to figure out how to update the container using Portainer. It turns out, it’s as simple as just pressing edit container and update container image. Here is a visual guide for my future self :-)</p>
<h3>Updating Docker Container</h3>
<p>First, go to your list of containers and select the container that needs to be updated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514835927/1a5ed9f6-6ea2-403f-999b-f75b78137fc8.png" alt="" /></p>
<p>Select “Duplicate/Edit” option.</p>
<p>Once you select “Edit”, you will have option for container image version.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514837734/a5f4bc22-9065-4e02-95c1-b98fc0d20caf.png" alt="" /></p>
<p>Select the container image version here.</p>
<p>In the “Image” section you can specify the version or keep <code>latest</code> or <code>stable</code> to get the latest image based on the container you are using. If you are unsure, go to the docker hub or source of the container to get the exact tag.</p>
<p>Finally, click on the “Deploy the container”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727514839253/c23156fe-e8dc-4775-85ac-07e4938e8ca0.png" alt="" /></p>
<p>Once done, deployment will trigger the image update.</p>
<p>This will pull the latest image and re-deploy the container with latest image.</p>
<p>⚠️ A few important caveats to keep in mind</p>
<ul>
<li>This assumes that the container configuration is persisted in the host machine, so that when it redeploys it will be able to resume from where you left off with the container app.</li>
<li>Downgrading may have unintended consequences.</li>
<li>Always look at the project/app documentation on upgrading if there is something special that needs to be done to migrate your app.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Using SQLDelight 2.0 with PostgreSQL for JVM]]></title><description><![CDATA[Recently I wanted to do some experiments with saving JSON data into a database and found out that PostgreSQL supports JSON as a data type.
I also wanted to use the fantastic SQLDelight library that cr]]></description><link>https://blog.hossain.dev/using-sqldelight-2-0-with-postgresql-for-jvm-10e749093a82</link><guid isPermaLink="true">https://blog.hossain.dev/using-sqldelight-2-0-with-postgresql-for-jvm-10e749093a82</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Wed, 06 Sep 2023 23:06:26 GMT</pubDate><content:encoded><![CDATA[<p>Recently I wanted to do some experiments with saving JSON data into a database and found out that PostgreSQL supports JSON as a data type.</p>
<p>I also wanted to use the fantastic SQLDelight library that creates type-safe models and queries for any database (it’s also multi-platform supported).</p>
<p>While trying to follow the recently released SQLDelight 2.0 <a href="https://cashapp.github.io/sqldelight/2.0.0/jvm_postgresql/">guide</a>, I stumbled into missing pieces to make the PostgreSQL work with it.</p>
<p>In this post, I will try to fill in the gaps and build a sample project showcasing both working together ✌️</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259349315/18b56528-97b0-45e9-9e12-576587a4be17.png" alt="" />

<p>PostgreSQL and SQLDelight. Source: <a href="https://kinsta.com/">https://kinsta.com/</a></p>
<p>A few of the missing pieces that I had to figure out were:</p>
<ol>
<li><p>Create <code>**DataSource**</code> (“hikari” or other connection managers were mentioned in the guide)</p>
</li>
<li><p>Using <code>**HikariCP**</code> as data source to connect to PostgreSQL</p>
</li>
<li><p>Write all the glue pieces to use the SQLDelight code to perform CRUD operations on the PostgreSQL Database</p>
</li>
</ol>
<p>Use <code>HikariCP</code> to create PostgreSQL <code>DataSource</code></p>
<p>First import the hikari and PostgreSQL library into your JVM project</p>
<pre><code class="language-kotlin">// https://github.com/brettwooldridge/HikariCP#artifacts
implementation("com.zaxxer:HikariCP:5.0.1")

// This is needed for the PostgreSQL driver
// https://mvnrepository.com/artifact/org.postgresql/postgresql
implementation("org.postgresql:postgresql:42.6.0")
</code></pre>
<p>Then you need to build the <code>HikariConfig</code> with the right data set to connect to the database and then create the <code>HikariDataSource</code> which is what is needed for the SQLDelight</p>
<p>Here is a snippet taken from the <a href="https://github.com/hossain-khan/SQLDelight-PostgreSQL-JVM-sample/blob/main/src/main/kotlin/dev/hossain/postgresqldelight/SportsRepository.kt">sample project</a></p>
<pre><code class="language-kotlin">private fun getDataSource(): DataSource {
    val hikariConfig = HikariConfig()
    // See https://jdbc.postgresql.org/documentation/use/
    hikariConfig.setJdbcUrl("jdbc:postgresql://localhost:5432/dbname")
    hikariConfig.driverClassName = "org.postgresql.Driver"
    hikariConfig.username = "dbusername"
    hikariConfig.password = "dbpassword"

    return HikariDataSource(hikariConfig)
}
</code></pre>
<p>That’s it, now you can follow the official SQLDelight guide on creating databse to create tables and do CRUD operations.</p>
<p>For example, here is a simplified snippet to give the whole picture</p>
<pre><code class="language-kotlin">val dataSource: DataSource = getDataSource(appConfig)

val driver: SqlDriver = dataSource.asJdbcDriver()

// NOTE: The `SportsDatabase` and `PlayerQueries` are from SQLDelight
val database = SportsDatabase(driver)
val playerQueries: PlayerQueries = database.playerQueries

val hockeyPlayers = playerQueries.selectAll().executeAsList()
println("Existing \({hockeyPlayers.size} records: \)hockeyPlayers")
// Prints following 👇
// - - - - - - - - - -
// Existing 15 records: 
// [HockeyPlayer(player_number=10, full_name=Corey Perry), ... ]
</code></pre>
<p>The full snippet is available <a href="https://github.com/hossain-khan/SQLDelight-PostgreSQL-JVM-sample/blob/main/src/main/kotlin/dev/hossain/postgresqldelight/SportsRepository.kt#L41-L53">here</a>.</p>
<p>See the GitHub project for a complete example with gradle dependencies and SQLDelight configuration needed to make it work.</p>
<p><a href="https://github.com/hossain-khan/SQLDelight-PostgreSQL-JVM-sample"><strong>GitHub - hossain-khan/SQLDelight-PostgreSQL-JVM-sample: A sample project exercising PostgreSQL with SQLDelight</strong><br /><em>A sample project exercising PostgreSQL with SQLDelight 2.0 - GitHub - hossain-khan/SQLDelight-PostgreSQL-JVM-sample</em>github.com</a></p>
<p>Happy to hear feedback or any corrections to do this in a better way.</p>
<blockquote>
<p>EDIT: After I wrote the article, I found similar article about it (which could have saved me ton of time), so do take a look at it too 😊</p>
</blockquote>
<p><a href="https://medium.com/@theendik00/sqldelight-for-postgresql-on-kotlin-jvm-b95d14d96134"><strong>SQLDelight for PostgreSQL on Kotlin JVM</strong><br />*In simple terms, SQLDelight is a tool that takes your slightly modified SQL code and converts it to easy to use Kotlin…*medium.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Kotlin coroutines error handling strategy — `runCatching` and `Result` class]]></title><description><![CDATA[I am trying to learn Kotlin coroutines, and was trying to learn more about how to handle errors from suspended functions. One of the recommended way by Google is to create a “Result” class like the fo]]></description><link>https://blog.hossain.dev/kotlin-coroutines-error-handling-strategy-runcatching-and-result-class-14e1467ced6</link><guid isPermaLink="true">https://blog.hossain.dev/kotlin-coroutines-error-handling-strategy-runcatching-and-result-class-14e1467ced6</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[runCatching]]></category><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sun, 02 May 2021 20:45:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259372641/df47ebe3-0328-4ac2-9b92-47f5be20e1cb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am trying to learn Kotlin coroutines, and was trying to learn more about how to handle errors from suspended functions. One of the <a href="https://developer.android.com/kotlin/coroutines">recommended way</a> by Google is to create a “Result” class like the following:</p>
<pre><code class="language-kotlin">sealed class Result&lt;out R&gt; {  
    data class Success&lt;out T&gt;(val data: T) : Result&lt;T&gt;()  
    data class Error(val exception: Exception) : Result&lt;Nothing&gt;()  
}
</code></pre>
<p>This allows us to take advantage of Kotlin’s <code>when</code> like following:</p>
<pre><code class="language-kotlin">when (result) {
    is Result.Success&lt;LoginResponse&gt; -&gt; // Happy path
    else -&gt; // Show error in UI
}
</code></pre>
<p>However, I have recently <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html">stumbled into</a> Kotlin’s <code>runCathing {}</code> API that makes use of native <code>Result&lt;T&gt;</code> class already <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/">available in standard lib</a> since Kotlin <code>v1.3</code></p>
<p>Here I will try to explore how the native API can replace the recommended example in the Android Kotlin <a href="https://developer.android.com/kotlin/coroutines">training guide</a> for <em>simple</em> use cases.</p>
<p>Here is a basic idea of how <code>runCatching {}</code> can be used from Android ViewModel.</p>
<p>Based on Kotlin standard lib <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html">doc</a>, you can use <code>runCatching { }</code> in 2 different ways. I will focus on one of them, since the concept for other one is similar.</p>
<p>To handle a function that may throw an exception in coroutines or regular function use this:</p>
<pre><code class="language-kotlin">val statusResult: Result&lt;String&gt; = runCatching {
    // function that may throw exception that needs to be handled
    repository.userStatusNetworkRequest(username)
}.onSuccess { status: String -&gt;
    println("User status is: $status")
}.onFailure { error: Throwable -&gt;
    println("Go network error: ${error.message}")
}

// Assuming following supposed* long running network API
suspend fun userStatusNetworkRequest(username: String) = "ACTIVE"
</code></pre>
<p>Notice the ‘<a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/">Result</a>’ returned from the <code>runCatching</code> this is where the power comes in to write semantic code to handle errors.</p>
<p>The <code>onSuccess</code> and <code>onFailrue</code> callback is part of <code>Result&lt;T&gt;</code> class that allows you to easily handle both cases.</p>
<h4>How to handle Exceptions</h4>
<p>In addition to nice callbacks, the <code>Result&lt;T&gt;</code> class provides multiple ways to recover from the error and provide a default value or fallback options.</p>
<ol>
<li><strong>Using</strong> <code>getOrDefault()</code> <strong>and</strong> <code>getOrNull()</code> <strong>API</strong></li>
</ol>
<pre><code class="language-kotlin">val status: String = statusResult.getOrDefault("STATUS_UNKNOWN") 

// Or if nullable data is acceptable use:
val status: String? = statusResult.getOrNull()
</code></pre>
<p>Since the <code>onSuccess</code> and <code>onFailure</code> returns <code>Result&lt;T&gt;</code> you can chain most of these API calls like following</p>
<pre><code class="language-kotlin">val status: String = runCatching {
    repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.getOrDefault("STATUS_UNKNOWN")
</code></pre>
<p><strong>2. Using</strong> <code>recover { }</code> <strong>API</strong></p>
<p>The <code>recover</code> API allows you to handle the error and recover from there with a fallback value of the same data type. See the following example.</p>
<pre><code class="language-kotlin">val status: Result&lt;String&gt; = runCatching {
    repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.recover { error: Throwable -&gt; "STATUS_UNKNOWN" }

println(status.isSuccess) // Prints "true" even if error is thrown
</code></pre>
<p><strong>3. Using</strong> <code>fold {}</code> <strong>API to map data</strong></p>
<p>The <code>fold</code> extension function allows you to map the error to a different data type you wish. In this example, I kept the user status as <code>String</code>.</p>
<pre><code class="language-kotlin">val status: String = runCatching {
    repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.fold(
    onSuccess = { status: String -&gt; status },
    onFailure = { error: Throwable -&gt; "STATUS_UNKNOWN" }
)
</code></pre>
<p>Aside from these, there are some additional useful functions and extension functions for <code>Result&lt;T&gt;</code> , take a look at <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/">official documentation</a> for more APIs.</p>
<p>I hope this was useful or a new discovery for you as it was for me 😊</p>
<p><strong>UPDATE #1:</strong> As Gabor has mentioned below, there is an unintended consequence about using it in coroutines. I will look into it and provide more updates on the usage soon. Thanks to Garbor for mentioning it.</p>
]]></content:encoded></item><item><title><![CDATA[Quick Trick — Use Android’s Animated Vector Drawable as ProgressBar]]></title><description><![CDATA[Preview of https://cdn.hashnode.com/res/hashnode/image/upload/v1626534581837/LrhWiqKZ1.html editing animated vector drawable (AVD).
Android’s ProgressBar widget comes with lot of customization control]]></description><link>https://blog.hossain.dev/quick-trick-use-androids-animated-vector-drawable-as-progressbar-9dd05587d4a0</link><guid isPermaLink="true">https://blog.hossain.dev/quick-trick-use-androids-animated-vector-drawable-as-progressbar-9dd05587d4a0</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Thu, 06 Aug 2020 04:33:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534583309/YuE4-ZA4O.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Preview of <a href="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534581837/LrhWiqKZ1.html">https://cdn.hashnode.com/res/hashnode/image/upload/v1626534581837/LrhWiqKZ1.html</a> editing animated vector drawable (AVD).</p>
<p>Android’s <a href="https://developer.android.com/reference/android/widget/ProgressBar">ProgressBar</a> widget comes with lot of customization controls and flexibility to set custom animated drawable. However, setting <a href="https://developer.android.com/reference/kotlin/android/graphics/drawable/AnimatedVectorDrawable?hl=en">AnimatedVectorDrawable</a> is not one of the option.</p>
<p>So, this is a quick trick on how to use custom ImageView to create indeterminate progress indicator using Animated Vector Drawable (AVD).</p>
<pre><code>class AvdLoadingProgressBar @JvmOverloads constructor*(
    *context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
*) *: AppCompatImageView*(*context, attrs, defStyleAttr*) {
    *private val avd = AnimatedVectorDrawableCompat.create*(*context, R.drawable.avd_anim*)*!!

    init *{
        *setImageDrawable*(*avd*)
        *avd.registerAnimationCallback*(*object : Animatable2Compat.AnimationCallback*() {
            *override fun onAnimationEnd*(*drawable: Drawable?*) {
                *post **{ **avd.start*() ***}
            ***}
        })
        *avd.start*()
    }
}*
</code></pre>
<pre><code class="language-kotlin">import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat

/**
 * Custom loading indicator using Animated vector drawable.
 *
 * ## External Resources
 * - https://medium.com/androiddevelopers/animation-jump-through-861f4f5b3de4
 * - https://gist.github.com/nickbutcher/97143b3240682e5c5851fe45b49fde93
 * - https://medium.com/androiddevelopers/re-animation-7869722af206
 * - https://gist.github.com/nickbutcher/b1806905c6bc0ef29f545fd580935bd3
 */
class AvdLoadingProgressBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    /*
     * NOTE: It can only return null if parsing error is found.
     * So, using `!!` operator should expose any issue with the AVD XML file in API 23 or lower.
     */
    private val avd = AnimatedVectorDrawableCompat.create(context, R.drawable.avd_anim_kijiji_loading)!!

    init {
        setImageDrawable(avd)
        avd.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
            override fun onAnimationEnd(drawable: Drawable?) {
                post { avd.start() }
            }
        })
        avd.start()
    }
}
</code></pre>
<pre><code class="language-xml">&lt;animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"&gt;
    &lt;aapt:attr name="android:drawable"&gt;
        &lt;vector
            android:name="vector"
            android:width="204dp"
            android:height="33dp"
            android:viewportWidth="204"
            android:viewportHeight="33"&gt;
            &lt;group android:name="group"&gt;
                &lt;path
                    android:name="a"
                    android:pathData="M 33 17 C 33 25.83 25.83 33 17 33 C 8.17 33 1 25.83 1 17 C 1 8.17 8.17 1 17 1 C 25.83 1 33 8.17 33 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_2"&gt;
                &lt;path
                    android:name="b"
                    android:pathData="M 75.59 17 C 75.59 25.83 68.42 33 59.59 33 C 50.76 33 43.59 25.83 43.59 17 C 43.59 8.17 50.76 1 59.59 1 C 68.42 1 75.59 8.17 75.59 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_4"&gt;
                &lt;path
                    android:name="c"
                    android:pathData="M 118.28 17 C 118.28 25.83 111.11 33 102.28 33 C 93.45 33 86.28 25.83 86.28 17 C 86.28 8.17 93.45 1 102.28 1 C 111.11 1 118.28 8.17 118.28 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_6"&gt;
                &lt;path
                    android:name="d"
                    android:pathData="M 160.78 17 C 160.78 25.83 153.61 33 144.78 33 C 135.95 33 128.78 25.83 128.78 17 C 128.78 8.17 135.95 1 144.78 1 C 153.61 1 160.78 8.17 160.78 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_8"&gt;
                &lt;path
                    android:name="e"
                    android:pathData="M 203.78 17 C 203.78 25.83 196.61 33 187.78 33 C 178.95 33 171.78 25.83 171.78 17 C 171.78 8.17 178.95 1 187.78 1 C 196.61 1 203.78 8.17 203.78 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
        &lt;/vector&gt;
    &lt;/aapt:attr&gt;
    &lt;target android:name="a"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="52"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#f8aa17"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="650"
                    android:duration="100"
                    android:valueFrom="#f8aa17"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="b"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="173"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#f1454f"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="750"
                    android:duration="100"
                    android:valueFrom="#f1454f"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="c"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="300"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#2681db"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="850"
                    android:duration="100"
                    android:valueFrom="#2681db"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="d"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="424"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#37a864"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="950"
                    android:duration="100"
                    android:valueFrom="#37a864"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="e"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="550"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#9b44ad"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="1050"
                    android:duration="100"
                    android:valueFrom="#9b44ad"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
&lt;/animated-vector&gt;
</code></pre>
<pre><code class="language-text">&lt;animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"&gt;
    &lt;aapt:attr name="android:drawable"&gt;
        &lt;vector
            android:name="vector"
            android:width="204dp"
            android:height="33dp"
            android:viewportWidth="204"
            android:viewportHeight="33"&gt;
            &lt;group android:name="group"&gt;
                &lt;path
                    android:name="a"
                    android:pathData="M 33 17 C 33 25.83 25.83 33 17 33 C 8.17 33 1 25.83 1 17 C 1 8.17 8.17 1 17 1 C 25.83 1 33 8.17 33 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_2"&gt;
                &lt;path
                    android:name="b"
                    android:pathData="M 75.59 17 C 75.59 25.83 68.42 33 59.59 33 C 50.76 33 43.59 25.83 43.59 17 C 43.59 8.17 50.76 1 59.59 1 C 68.42 1 75.59 8.17 75.59 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_4"&gt;
                &lt;path
                    android:name="c"
                    android:pathData="M 118.28 17 C 118.28 25.83 111.11 33 102.28 33 C 93.45 33 86.28 25.83 86.28 17 C 86.28 8.17 93.45 1 102.28 1 C 111.11 1 118.28 8.17 118.28 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_6"&gt;
                &lt;path
                    android:name="d"
                    android:pathData="M 160.78 17 C 160.78 25.83 153.61 33 144.78 33 C 135.95 33 128.78 25.83 128.78 17 C 128.78 8.17 135.95 1 144.78 1 C 153.61 1 160.78 8.17 160.78 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
            &lt;group android:name="group_8"&gt;
                &lt;path
                    android:name="e"
                    android:pathData="M 203.78 17 C 203.78 25.83 196.61 33 187.78 33 C 178.95 33 171.78 25.83 171.78 17 C 171.78 8.17 178.95 1 187.78 1 C 196.61 1 203.78 8.17 203.78 17 Z"
                    android:fillColor="#373373"
                    android:strokeWidth="1"/&gt;
            &lt;/group&gt;
        &lt;/vector&gt;
    &lt;/aapt:attr&gt;
    &lt;target android:name="a"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="52"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#f8aa17"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="650"
                    android:duration="100"
                    android:valueFrom="#f8aa17"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="b"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="173"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#f1454f"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="750"
                    android:duration="100"
                    android:valueFrom="#f1454f"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="c"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="300"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#2681db"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="850"
                    android:duration="100"
                    android:valueFrom="#2681db"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="d"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="424"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#37a864"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="950"
                    android:duration="100"
                    android:valueFrom="#37a864"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
    &lt;target android:name="e"&gt;
        &lt;aapt:attr name="android:animation"&gt;
            &lt;set&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="550"
                    android:duration="100"
                    android:valueFrom="#373373"
                    android:valueTo="#9b44ad"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
                &lt;objectAnimator
                    android:propertyName="fillColor"
                    android:startOffset="1050"
                    android:duration="100"
                    android:valueFrom="#9b44ad"
                    android:valueTo="#373373"
                    android:valueType="colorType"
                    android:interpolator="@android:interpolator/fast_out_slow_in"/&gt;
            &lt;/set&gt;
        &lt;/aapt:attr&gt;
    &lt;/target&gt;
&lt;/animated-vector&gt;
</code></pre>
<blockquote>
<p>NOTE: The idea of repeated indefinite AVD is heavily borrowed from Nick Butcher’s medium article on AVD. I highly recommend you read them to know more tips and ticks with AVD.</p>
</blockquote>
<p>That’s it. You can now use this in your layout as usual view and show it when data is loading from network or any long running async request is happening.</p>
<pre><code>*&lt;*androidx.constraintlayout.widget.ConstraintLayout&gt;

  &lt;dev.hossain.avdprogress.AvdLoadingProgressBar
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/avd_anim_kijiji_loading" /&gt;

&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
</code></pre>
<p><img src="https://cdn-images-1.medium.com/max/2000/1*kJDC8wIDX1tV5ah4Y-xehw.gif" alt="Here is preview of the AVD in https://shapeshifter.design/" /><em>Here is preview of the AVD in <a href="https://shapeshifter.design/">https://shapeshifter.design/</a></em></p>
<h3>Further reading</h3>
<p><a href="https://medium.com/androiddevelopers/animation-jump-through-861f4f5b3de4"><strong>Animation: Jump-through</strong>
*Recently a call for help caught my eye; asking how to implement a fancy ‘getting location’ animation on Android:*medium.com</a>
<a href="https://medium.com/androiddevelopers/re-animation-7869722af206"><strong>Re-animation</strong>
*In a previous article, I described a technique for creating vector animations on Android:*medium.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Source code syntax highlighting on Android — Taking full control]]></title><description><![CDATA[Android and it’s community has evolved a lot over past decade. Now a days we can find open-source libraries to do almost anything — Zoomable ImageView, CameraX, RecyclerView Sticky Header, Tooltip, an]]></description><link>https://blog.hossain.dev/source-code-syntax-highlighting-on-android-taking-full-control-b704fd4bd8ee</link><guid isPermaLink="true">https://blog.hossain.dev/source-code-syntax-highlighting-on-android-taking-full-control-b704fd4bd8ee</guid><category><![CDATA[Android]]></category><category><![CDATA[webview]]></category><category><![CDATA[syntax highlighting]]></category><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sat, 18 Jul 2020 22:13:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534609145/0XPq0AgZ8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Android and it’s community has evolved a lot over past decade. Now a days we can find open-source libraries to do almost anything — Zoomable ImageView, CameraX, RecyclerView Sticky Header, Tooltip, and many more.</p>
<img src="https://cdn-images-1.medium.com/max/10368/0*Ywlp-1OAf-_EgtyR" alt="Photo by Shahadat Rahman on Unsplash" />

<p><em>Photo by</em> <a href="https://unsplash.com/@hishahadat?utm_source=medium&amp;utm_medium=referral"><em>Shahadat Rahman</em></a> <em>on</em> <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral"><em>Unsplash</em></a></p>
<p>Syntax highlighting is one of them and there are <a href="https://github.com/kbiakov/CodeView-Android">few</a><a href="https://github.com/PDDStudio/highlightjs-android">libraries</a><a href="https://github.com/Badranh/Syntax-View-Android">for</a><a href="https://github.com/testica/codeeditor">that</a> too. If this is a solved problem, then why am I writing about another solution then?</p>
<p>Well, some of the libraries I have explored are outdated, and some of them might not be as feature rich. So, I wanted to explore how to do-it-myself and write about it so that anybody can take full advantage of well-know JavaScript libraries for syntax highlighting in their Android app.</p>
<p>👍 <strong>Pros</strong></p>
<ul>
<li><p>Complete control over how JS plugin is used in the app</p>
</li>
<li><p>Complete control over the Android code that renders it using <code>WebView</code></p>
</li>
</ul>
<p>👎 <strong>Cons</strong></p>
<ul>
<li><p>You need to have some understanding of web technologies, namely HTML, CSS and JavaScript</p>
</li>
<li><p>You need to build your own Android <code>CustomView</code> or <code>Fragment</code> to render highlighted syntax. <em>(No worries — this is where this article comes in to help you do that 🤗)</em></p>
</li>
</ul>
<p>With the pros and cons in mind, lets explore some well-established and proven syntax highlighting libraries</p>
<ul>
<li><p><a href="https://prismjs.com/index.html">PrismJS</a> — Very light weight <code>2KB-100+KB</code> with extensive plugin collection “<em>Lightweight, robust, elegant syntax highlighting.</em>”</p>
</li>
<li><p><a href="https://highlightjs.org/">highlight.js</a> — Fully loaded <code>25KB-100KB</code> and simple syntax highlighting “<em>Syntax highlighting for the Web”</em></p>
</li>
<li><p><a href="https://craig.is/making/rainbows">Rainbow</a> — Another popular lightweight <code>6KB-25KB</code> highlighter “<em>Rainbow is a code syntax highlighting library written in Javascript.</em>”</p>
</li>
</ul>
<p>There are many more libraries that does the job, however for rest of the example I will be using PrismJS because of it’s highly modular nature. Note that, the process of using different library will be <em>almost</em> the same.</p>
<blockquote>
<p>💡 TIP: All the example code provided here is available in great detail in the GitHub repository mentioned below👇.</p>
</blockquote>
<h3>Setting up PrismJS Template</h3>
<p>We will be leveraging Android’s<code>WebView</code> to load syntax highlighting library with the source code that we want to be highlighted.</p>
<p>This setup is going to be specific to library of your choice. Since we are focusing on PrimsJS, we will follow it’s <a href="https://prismjs.com/#basic-usage">official guideline</a>.</p>
<p>After you <a href="https://prismjs.com/download.html">download</a> the library JS and CSS file, you essentially need following HTML to render the highlighted source.</p>
<pre><code class="language-kotlin">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link href="themes/prism.css" rel="stylesheet" /&gt;
    &lt;script src="prism.js"&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;pre&gt;&lt;code class="language-kotlin"&gt;
    data class Student(val name: String)
    &lt;/code&gt;&lt;/pre&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<blockquote>
<p>TIP: You can download different CSS theme, and you can choose list of language and plugin you want to support (eg. Kotlin, Show line number)</p>
</blockquote>
<p>Put the downloaded files in your Android app’s assets directory, ideally in www subfolder, like — <code>src/main/assets/www/</code></p>
<p>Now that we have a baseline for the template, next part is to convert it to Kotlin function that can take additional parameter to customize different plugin options. Here is template with some additional data needed for mobile.</p>
<pre><code class="language-kotlin">fun prismJsHtmlContent(
    formattedSourceCode: String,
    language: String,
    showLineNumbers: Boolean = true
): String {
    return """&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;

    &lt;link href="www/prism.css" rel="stylesheet"/&gt;
    &lt;script src="www/prism.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;pre class="${if (**showLineNumbers**) "line-numbers" else ""}"&gt;
&lt;code class="language-\({**language**}"&gt;\){**formattedSourceCode**}&lt;/code&gt;
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;
"""
}
</code></pre>
<h3>Defining Syntax Highlighting Custom-View</h3>
<p>Making your own custom view is a great way to enhance capabilities. In this case we want to make our custom-view extend from <code>WebView</code> so that we can load the template we just defined with source code at runtime.</p>
<pre><code class="language-kotlin">package your.app.code

class SyntaxHighlighterWebView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
    companion object {
        private const val ANDROID_ASSETS_PATH = "file:///android_asset/"
    }

    // Our exposed function to show highlighted syntax
    fun bindSyntaxHighlighter(
        formattedSourceCode: String,
        language: String,
        showLineNumbers: Boolean = false
    ) {
        settings.javaScriptEnabled = true

         loadDataWithBaseURL(
            ANDROID_ASSETS_PATH /* baseUrl */,
            prismJsHtmlContent(
               formattedSourceCode, 
               language, 
               showLineNumbers
            ) /* html-data */,
            "text/html" /* mimeType */,
            "utf-8" /* encoding */,
            "" /* failUrl */
        )
    }
}
</code></pre>
<h3>Using the Custom-View in App</h3>
<p>Now that we have our <code>SyntaxHighlighterWebView</code> custom-view with <code>bindSyntaxHighlighter()</code> function, we can use it from Activity or Fragment layout.</p>
<p>All we need to do is use the view in XML layout like following:</p>
<pre><code class="language-xml">&lt;your.app.code.SyntaxHighlighterWebView
  android:id="@+id/syntax_highlighter_webview"
  android:layout_width="match_parent"
  android:layout_height="match_parent" /&gt;
</code></pre>
<p>And from your <code>Activity</code> or <code>Fragment</code> get reference to the view and bind the source code like following</p>
<pre><code class="language-kotlin">val highlighter: SyntaxHighlighterWebView = findViewById(R.id.syntax_highlighter_webview)

highlighter.bindSyntaxHighlighter(
   formattedSourceCode = "data class Student(val name: String)",
   language = "kotlin",
   showLineNumbers = true
)
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534604566/je1MVaJvh.png" alt="Highlighted Syntax Loaded via SyntaxHighlighterWebView" />

<p><em>Highlighted Syntax Loaded via SyntaxHighlighterWebView</em></p>
<p>That’s it, once you load the app you should see highlighted syntax on the screen where you have put the custom-view.</p>
<p>All the example source code provided here is available in following GitHub repository. As bonus, I have also provided example of how to use highlight.js too.</p>
<p>If you find any issue, leave a comment here or report an issue at GitHub repository. Hope it helps somebody. ✌️ <a href="https://github.com/amardeshbd/android-syntax-highlighter"><strong>amardeshbd/android-syntax-highlighter</strong> *Yet Another Android Syntax Highlighter (YAASH). Example of how to use any JavaScript library to enable syntax highlighting in your android app.*github.com</a></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534606655/gn8XzpbS2.jpeg" alt="Set of screenshots taken from the demo app" />

<p><em>Set of screenshots taken from the demo app</em></p>
]]></content:encoded></item><item><title><![CDATA[Setup Android Gradle based Firebase App Distribution with GitHub Actions CI]]></title><description><![CDATA[This is a quick guide on how you can easily set up Github Actions CI workflow to automatically post APK to Firebase App Distribution on merge to release or master (or soon to be known as main) branch.
Firebase already has an excellent guide 🏆 on how...]]></description><link>https://blog.hossain.dev/setup-android-gradle-based-firebase-app-distribution-with-github-actions-ci-a7803269a4e1</link><guid isPermaLink="true">https://blog.hossain.dev/setup-android-gradle-based-firebase-app-distribution-with-github-actions-ci-a7803269a4e1</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Tue, 16 Jun 2020 12:50:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259355440/50585ee4-2be1-4e06-8f6f-ad6d5679c2a3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a quick guide on how you can easily set up Github Actions CI workflow to automatically post APK to <a target="_blank" href="https://firebase.google.com/docs/app-distribution">Firebase App Distribution</a> on merge to <code>release</code> or <code>master</code> (or soon to be known as <code>main</code>) branch.</p>
<p>Firebase already has an <a target="_blank" href="https://firebase.google.com/docs/app-distribution/android/distribute-gradle"><strong>excellent guide</strong></a> 🏆 on how to set up the Gradle task on your Android project to post APK to App Distribution. However, I will quickly touch those areas using the easiest path ✌️.</p>
<blockquote>
<p><strong>PREREQUISITE</strong>: You already have Firebase project setup for the app with <code>google-services.json</code> file in your Android app project.</p>
</blockquote>
<h4 id="heading-setup-gradle-for-the-app">Setup Gradle for the App</h4>
<p>On your root <code>build.gradle</code> file add the Gradle plugin</p>
<pre><code class="lang-kotlin">dependencies {
<span class="hljs-comment">// .. more existing dependencies here</span>
classpath <span class="hljs-string">'com.google.firebase:firebase-appdistribution-gradle:2.0.0'</span> <span class="hljs-comment">// see official guide for latest versions</span>
}
</code></pre>
<p>Next, apply the plugin in your Android app’s <code>build.gradle</code> and distribution properties. See the <a target="_blank" href="https://firebase.google.com/docs/app-distribution/android/distribute-gradle#step_3_configure_your_distribution_properties">official guide</a> for the full list of supported parameters.</p>
<pre><code class="lang-plaintext">apply plugin: 'com.google.firebase.appdistribution'

android {
    buildTypes {
        release { // NOTE: `debug` can have different cofigs
            firebaseAppDistribution {
                releaseNotes="Release notes at bit.ly/notes"
                groups="qa" // see docs for more options 
            }
        }
    }
}
</code></pre>
<h4 id="heading-generate-firebase-token">Generate Firebase Token</h4>
<p>Firebase has <a target="_blank" href="https://firebase.google.com/docs/app-distribution/android/distribute-gradle#step_2_authenticate_with_firebase">3 different documented ways</a> you can authenticate to be able to upload the APK. Generating the token is one of the easiest that I will focus on with snapshot images so that you can easily relate to it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259352284/7a94c13b-f6e2-429e-b7e5-81427cd866a8.png" alt /></p>
<p>From the root of your android app project, run following Gradle command which will give you a URL to authenticate for the Firebase project that app uses.</p>
<p>./gradlew appDistributionLogin</p>
<p>Copy the URL <code>https://accounts.google.com/o/..../auth/cloud-platform</code> and paste it in browser and login with the Google account which has write access to the Firebase project.</p>
<p>Once authorized, you should get `FIREBASE_TOKEN` in the console. Save it for later use.</p>
<h4 id="heading-setup-secrets-for-github-ci-workflow">Setup Secrets for GitHub CI Workflow</h4>
<p>Go to your GitHub project and the <code>FIREBASE_TOKEN</code> as secret property.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259353928/5d9b4ee5-eb28-46f5-8923-e9aac1115564.png" alt /></p>
<h4 id="heading-setup-github-actions-ci-workflow">Setup GitHub Actions CI Workflow</h4>
<p>Based on git-flow, we want to setup the CI job such that whenever we merge commit to <code>master</code> branch it triggers the release build. You can obviously <a target="_blank" href="https://help.github.com/en/actions/reference/events-that-trigger-workflows">customize the behavior</a> based on your need.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Firebase</span> <span class="hljs-string">App</span> <span class="hljs-string">Distribution</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span> <span class="hljs-number">1.8</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-number">1.8</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Firebase</span> <span class="hljs-string">App</span> <span class="hljs-string">Distribute</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">FIREBASE_TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.FIREBASE_TOKEN</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./gradlew</span> <span class="hljs-string">assembleRelease</span> <span class="hljs-string">appDistributionUploadRelease</span>
</code></pre>
<p>That’s it, you should have a working GitHub workflow that automatically sends APK to Firebase App Distribution as soon as there is a commit on <code>master</code> branch.</p>
<p>See <a target="_blank" href="https://github.com/amardeshbd/android-police-brutality-incidents/pull/117/files">pull-request I made</a> to add support for this in one of my side projects.</p>
<p>If you find any issues please let me know I will try to help. Good luck 👍</p>
<h4 id="heading-additional-resources">Additional Resources</h4>
<ul>
<li><p>Firebase App Distribution — <a target="_blank" href="https://firebase.google.com/docs/app-distribution">https://firebase.google.com/docs/app-distribution</a></p>
</li>
<li><p>GitHub Actions — <a target="_blank" href="https://help.github.com/en/actions">https://help.github.com/en/actions</a></p>
</li>
<li><p>Pull Request to support Firebase App Distribution — <a target="_blank" href="https://github.com/amardeshbd/android-police-brutality-incidents/pull/117/files">https://github.com/amardeshbd/android-police-brutality-incidents/pull/117/files</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Hackathon: Creating the simplest Muzei Wallpaper plugin for Android]]></title><description><![CDATA[A snapshot of the ‘H.K. Vision’ Muzei Plugin on Android Device — Available as BETA release on Google Play
I have a hobby of taking a lot of pictures, and some of the pictures do turn out nice (at least to me 😊). So, I’ve also created a web portal to...]]></description><link>https://blog.hossain.dev/hackathon-creating-the-simplest-muzei-wallpaper-plugin-for-android-9d080dbb4bf</link><guid isPermaLink="true">https://blog.hossain.dev/hackathon-creating-the-simplest-muzei-wallpaper-plugin-for-android-9d080dbb4bf</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sun, 24 May 2020 06:33:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534577165/8OCeYVQ4M.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A snapshot of the ‘H.K. Vision’ Muzei Plugin on Android Device — Available as BETA release on Google Play</p>
<p>I have a hobby of taking a lot of pictures, and some of the pictures do turn out nice (<em>at least to me</em> 😊). So, I’ve also created a <a target="_blank" href="https://vision.hossainkhan.com/">web portal</a> to showcase those pictures 🖼️.</p>
<p>As an Android engineer, I was aware of <a target="_blank" href="https://play.google.com/store/apps/details?id=net.nurik.roman.muzei">**Muzei Live Wallpaper</a>** app created by <a target="_blank" href="https://twitter.com/romannurik">Roman Nurik</a> back in 2014. Recently I’ve seen some updates from <a target="_blank" href="https://twitter.com/ianhlake">Ian Lake</a> who took over the project while ago, so I decided to make a plugin for Muzei to use my pictures as wallpaper on my Android phone.</p>
<blockquote>
<p>ps. If you simply want to use the plugin, see <a target="_blank" href="https://github.com/amardeshbd/android-hk-vision-muzei-plugin/blob/master/README.md">README</a> for instruction 🤗</p>
</blockquote>
<p>The <strong><em>objective of this post</em></strong> is to showcase how easy it was for <strong><em>me</em></strong> to work on this ⏰ 1-day hackathon project to create the plugin for Muzei by following <a target="_blank" href="https://github.com/romannurik/muzei/blob/master/muzei-api/module.md">guidelines</a> and examples provided in the Muzei <a target="_blank" href="https://github.com/romannurik/muzei">GitHub repository</a>.</p>
<p>Essentially, I had to do 2 major tasks to create the Android plugin</p>
<ol>
<li><p>Download a list of image URLs+metadata and convert them into <code>Artwork</code> object.</p>
</li>
<li><p>Declare a content provider on <code>AndroidManifest.xml</code> to expose the images for Muzei Live Wallpaper app.</p>
</li>
</ol>
<h3 id="download-image-metadata">Download Image Metadata</h3>
<p>You can use any library of your choice to do that, I’ve followed the <a target="_blank" href="https://github.com/romannurik/muzei/tree/master/example-unsplash/src/main/java/com/example/muzei/unsplash">unsplash example</a> and used Retrofit+WorkManager to download metadata and convert the images into <code>Artwork</code> objects. See <a target="_blank" href="https://github.com/amardeshbd/android-hk-vision-muzei-plugin/blob/master/app/src/main/java/com/hossainkhan/vision/data/HkVisionWorker.kt">source code snapshot</a> from GitHub.</p>
<h3 id="expose-content-provider">Expose Content Provider</h3>
<p>Based on <code>MuzeiArtProvider</code> <a target="_blank" href="https://api.muzei.co/reference/com.google.android.apps.muzei.api.provider/-muzei-art-provider/index.html">documentation</a>, I extended my <code>[HkVisionArtProvider</code>](https://github.com/amardeshbd/android-hk-vision-muzei-plugin/blob/master/app/src/main/java/com/hossainkhan/vision/muzei/HkVisionArtProvider.kt) from it and defined following provider specification in <code>AndroidManifest.xml</code></p>
<p><img src="https://cdn-images-1.medium.com/max/3464/1*Wc_NtCluWXrOokPaWsGICg.png" alt="[Source code](https://cdn.hashnode.com/res/hashnode/image/upload/v1626534575029/kOeMktqVA6.html) available at the GitHub repository" /><em><a target="_blank" href="https://github.com/amardeshbd/android-hk-vision-muzei-plugin/blob/master/app/src/main/AndroidManifest.xml#L15">Source code</a> available at the GitHub repository</em></p>
<p>That’s it. Once you build and install the APK, the source will show up in the Muzei Live Wallpaper <a target="_blank" href="https://play.google.com/store/apps/details?id=net.nurik.roman.muzei">app</a> which allows you to choose a wallpaper from a list of photos that was added to Muzei artwork <a target="_blank" href="http://api.muzei.co/reference/com.google.android.apps.muzei.api.provider/-provider-client/index.html">provider client</a>.</p>
<p>I have released the plugin app on Google Play as a <em>public</em> release. If you are interested, then <a target="_blank" href="https://play.google.com/store/apps/details?id=com.hossainkhan.vision">get it on Google Play</a>. If you do like the wallpapers, feel free to drop a line here.✌️</p>
<p>💻 Source code of the Android project is available at GitHub repo below:
<a target="_blank" href="https://github.com/amardeshbd/android-hk-vision-muzei-plugin"><strong>amardeshbd/android-hk-vision-muzei-plugin</strong>
<em>Muzei Live Wallpaper app’s photo data source plugin for vision.hossainkhan.com website. </em>github.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Use node.js tools on GitHub actions CI workflow]]></title><description><![CDATA[GitHub Actions Continuous Integration (CI) Workflow
On April 14th, 2020 GitHub announced a major change in their plans to allow free private repositories. It’s a good time to make use of their free CI/CD known as “Actions”. Private repositories have ...]]></description><link>https://blog.hossain.dev/use-node-js-tools-on-github-actions-ci-workflow-120fb3b4a3e1</link><guid isPermaLink="true">https://blog.hossain.dev/use-node-js-tools-on-github-actions-ci-workflow-120fb3b4a3e1</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Sat, 23 May 2020 13:21:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259359973/bb18d5d5-ce07-4e55-9f6b-ab5ee392e892.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>GitHub Actions Continuous Integration (CI) Workflow</p>
<p>On April 14th, 2020 GitHub <a target="_blank" href="https://github.blog/2020-04-14-github-is-now-free-for-teams/">announced</a> a major change in their plans to allow free private repositories. It’s a good time to make use of their free CI/CD known as “<a target="_blank" href="https://github.com/features/actions">Actions</a>”. Private repositories have 2000mins/month and public repositories have unlimited minutes (<em>see</em> <a target="_blank" href="https://github.com/pricing"><em>pricing</em></a>).</p>
<p>This is a quick write-up on how you can leverage Actions CI Workflow to do any task using supported node.js modules from <a target="_blank" href="https://www.npmjs.com/">npm</a>.</p>
<blockquote>
<p>NOTE: If you are new to GitHub Actions, I highly recommend reading their ‘<a target="_blank" href="https://help.github.com/en/actions/getting-started-with-github-actions">Getting Started</a>’ guide first.</p>
</blockquote>
<p>The concept is simple, you setup node environment on CI machine, install required CLI-based node modules, and run your tests.</p>
<blockquote>
<p>NOTE: Use node package manager portal at <a target="_blank" href="https://www.npmjs.com/">npmjs.com</a> to find your node-module to run on GitHub Actions CI job.</p>
</blockquote>
<p>Use case #1: Use XML Validator module to validate XML. See <a target="_blank" href="https://github.com/amardeshbd/android-hk-vision-muzei-plugin/blob/master/.github/workflows/android.yml">workflow file</a>.</p>
<ul>
<li><p>name: Setup NodeJS<br />uses: actions/setup-node@v1<br />with:<br />  node-version: 12.x  </p>
</li>
<li><p>name: Install XML Validator<br />run: npm install -g fast-xml-parser  </p>
</li>
<li>name: Validate AndroidManifest.xml<br />run: xml2js -V app/src/main/AndroidManifest.xml</li>
</ul>
<p>Use case #2: Validate JSON file using JSON-Schema specification (<a target="_blank" href="https://github.com/amardeshbd/vision.hossainkhan.com/blob/master/.github/workflows/validate-json-nodejs.yml">source</a>).</p>
<ul>
<li><p>name: Setup NodeJS<br />uses: actions/setup-node@v1<br />with:<br />  node-version: 12.x</p>
</li>
<li><p>run: npm install -g ajv-cli  </p>
</li>
<li>run: ajv validate -s photos-schema.json -d photos.json</li>
</ul>
<p>That’s it, now you have your own workflow to validate or do other tasks on GitHub repository.</p>
]]></content:encoded></item><item><title><![CDATA[How to take your beginner Android skills to the next level by studying open-source Android Apps]]></title><description><![CDATA[This list of open-source Android apps may come in handy if you have grasped all necessary concepts to develop Android application and you think you are ready to work on an application that follows industry standards. By industry standards, I mean, an...]]></description><link>https://blog.hossain.dev/how-to-take-your-beginner-android-skills-to-the-next-level-by-studying-open-source-android-apps-713d55c5094</link><guid isPermaLink="true">https://blog.hossain.dev/how-to-take-your-beginner-android-skills-to-the-next-level-by-studying-open-source-android-apps-713d55c5094</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Thu, 02 Apr 2020 03:06:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534599140/HCFNw4031.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This list of open-source Android apps may come in handy if you have grasped all necessary concepts to develop Android application and you think you are ready to work on an application that follows industry standards. By industry standards, I mean, an app that has good architecture, is scalable and maintainable in the long run.</p>
<p>Here is the list of open-source apps in this article:</p>
<ul>
<li><p><strong>Android Architecture Blueprints v2</strong> — <em>Google’s official recommended app</em></p>
</li>
<li><p><strong>Plaid 2.0 </strong>— <em>Google’s official recommended app with focus on Material Design</em></p>
</li>
<li><p><strong>Sunflower</strong> — <em>Google’s official recommended app with more focus on many Jetpack components</em></p>
</li>
<li><p><strong>CatchUp</strong> — <em>Community app with multi-module and multi-api-service</em></p>
</li>
<li><p><strong>Showcase</strong> — <em>Community app with multi-module and clean architecture</em></p>
</li>
<li><p><strong>Google I/O </strong>— <em>Google’s official annual conference app</em></p>
</li>
<li><p><strong>Tivi</strong> — <em>Community app with a focus on multi-module architecture and Jetpack</em></p>
<blockquote>
<p>FYI: Google has an 📚<a target="_blank" href="https://developer.android.com/jetpack/docs/guide"> **official architecture guide for Android application</a>** which captures what most of the sample application does. I highly recommend you read that article first before studying any open-source applications.</p>
</blockquote>
</li>
</ul>
<h2 id="android-architecture-blueprints-v2">⚙️ Android Architecture Blueprints v2</h2>
<p>This is Google’s official app that showcases the usage of some of the key <a target="_blank" href="https://developer.android.com/jetpack">Jetpack</a> components to make a sustainable app. This is a good starting point to get an understanding of how to architect an app.</p>
<p>They do have a vanilla implementation in <code>master</code> branch, however, I will focus on <code>dagger-android</code> branch that has dagger support <em>(see README)</em>.</p>
<ul>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture:</strong> MVVM</p>
</li>
<li><p><strong>Dependency Injection:</strong> Dagger</p>
</li>
<li><p><strong>Navigation:</strong> Jetpack Navigation</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit and Espresso</p>
</li>
<li><p><strong>Source:</strong> <a target="_blank" href="https://github.com/android/architecture-samples">https://github.com/android/architecture-samples</a></p>
</li>
</ul>
<h2 id="plaid-20-showcasing-material-design">👔 Plaid 2.0 — Showcasing Material Design</h2>
<p>In early days Plaid 1.0 application (created in 2014) was <a target="_blank" href="https://twitter.com/crafty">Nick Butcher</a>’s app where he showcased how <a target="_blank" href="https://material.io/">material design</a> and animation can bring joy and life to an Android application. After years of improving, the Plaid app has come to a point where it makes perfect sense to make this app a reference app that showcases how an ideal Android application can be built using material design and fluid animation. So, in 2019, Nick did exactly that, they have moved the Plaid Github repo to Google’s official repository — <a target="_blank" href="https://medium.com/@crafty/restitching-plaid-9ca5588d3b0a">here is the article explaining the move and goal</a> <em>(I highly recommend to read it)</em>.</p>
<p>NOTE: The Plaid 2.0 is still under heavy development, which as a bonus, gives you an opportunity to learn how an application is migrated to modern architecture and Kotlin. See the Github project page with different technical articles explaining how the app is being migrated to 2.0</p>
<ul>
<li><p><strong>Key Features:</strong> Material Design, Android Theming, Dark Mode, Multi-Module, Animation</p>
</li>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture:</strong> MVVM</p>
</li>
<li><p><strong>Dependency Injection:</strong> Dagger</p>
</li>
<li><p><strong>Navigation:</strong> Plain (intent based)</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit and Espresso</p>
</li>
<li><p><strong>Source: [</strong>https://github.com/android/plaid](https://github.com/android/plaid)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534587509/Xwbqom4j0.jpeg" alt="Snapshot of Plaid 1.0 — The latest 2.0 app is still under heavy development." /><em>Snapshot of Plaid 1.0 — The latest 2.0 app is still under heavy development.</em></p>
<h2 id="sunflower-showcasing-android-jetpack">🌻 Sunflower — Showcasing Android Jetpack</h2>
<p>This is another Google’s official app that showcases many Jetpack components in one application.</p>
<p>This is a minimal application that is great for learning.</p>
<ul>
<li><p><strong>Key Features:</strong> Dark Mode, Animation, Room (Database), WorkManager</p>
</li>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture:</strong> MVVM (LiveData, ViewModel, Lifecycle, Data Binding)</p>
</li>
<li><p><strong>Dependency Injection:</strong> None (Dagger not used to keep it simple)</p>
</li>
<li><p><strong>Navigation:</strong> Jetpack Navigation (Single Activity)</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit and Espresso</p>
</li>
<li><p><strong>Source: [</strong>https://github.com/android/sunflower](https://github.com/android/sunflower)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534589720/p4xvNDEic.png" alt="Sunflower — Demo Screenshots" /><em>Sunflower — Demo Screenshots</em></p>
<h2 id="catchup-all-in-one">🔖 CatchUp — All in one</h2>
<p>This app aggregates articles and posts from different services like Hackernews, Medium, Reddit, Slashdot, Dribble, Uplabs and so on. This is a very recent app from <a target="_blank" href="https://www.zacsweers.dev/">Zac Sweers</a> who has put a significant amount of time to develop this app. The app architecture is <a target="_blank" href="https://github.com/ZacSweers/CatchUp#influences">inspired</a> by Plaid and U+2020 app. CatchUp is being actively developed, you can clone and build locally to try it out.</p>
<p>Please note, this is kinda large scale complex application that is well done and contains many advanced techniques. So, if you are a beginner, I would postpone looking into the application towards the end of your study ^_^</p>
<blockquote>
<p>ps. I, myself have <a target="_blank" href="https://twitter.com/rharter/status/1240773309316378626?s=20">found</a> this application recently. Personally I like how the Gradle script is organized using Kotlin based Gradle scripts (kts), and how the dagger is used extensively to manage dependencies of different component and service implementations.</p>
</blockquote>
<ul>
<li><p><strong>Key Features:</strong> Dark Mode, Animation, Advanced Dagger,</p>
</li>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture: </strong>— (Not sure, I need to study this app more)</p>
</li>
<li><p><strong>Dependency Injection:</strong> Dagger Hilt (Advanced usage)</p>
</li>
<li><p><strong>Navigation: </strong>Basic (Intent Based)</p>
</li>
<li><p><strong>Unit Tests:</strong> Some JUnit tests exist (not priority).</p>
</li>
<li><p><strong>Source: [</strong>https://github.com/ZacSweers/CatchUp](https://github.com/ZacSweers/CatchUp)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534592061/lxaIt78fV.png" alt="CatchUP — Demo Screenshots" /><em>CatchUP — Demo Screenshots</em></p>
<blockquote>
<p>NOTE: As per Zac, he recently <a target="_blank" href="https://twitter.com/ZacSweers/status/1274830421835100162?s=20">tweeted</a> “standard disclaimer that CatchUp is not necessarily a template of patterns I endorse or recommend. It’s a laboratory not a representative sample”</p>
</blockquote>
<h2 id="showcase-clean-architecture">Showcase — clean architecture</h2>
<p>This is another community sample that I discovered recently. The sample app is developed by <a target="_blank" href="https://twitter.com/igorwojda">Igor Wojda</a>, author of “<em>Android Development with Kotlin</em>” <a target="_blank" href="https://www.amazon.ca/Android-Development-Kotlin-Marcin-Moskala/dp/1787123685">book</a>.</p>
<p>The showcase app incorporates many best practices, here is the project summary on Igor’s own words:</p>
<blockquote>
<p>Android application following best practices: Kotlin, coroutines, Clean Architecture, feature modules, tests, MVVM, static analysis.</p>
</blockquote>
<ul>
<li><p><strong>Key Features:</strong> Kotlin and Coroutines, Gradle KTS, Static Code Analysis</p>
</li>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture:</strong> Clean Architecture</p>
</li>
<li><p><strong>Dependency Injection: </strong>None (Manual)</p>
</li>
<li><p><strong>Navigation:</strong> Jetpack Navigation</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit</p>
</li>
<li><p><strong>Source: [</strong>https://github.com/igorwojda/android-showcase](https://github.com/igorwojda/android-showcase)</p>
</li>
</ul>
<h2 id="honorable-mention">Honorable mention</h2>
<p>Here are some open-source apps that are worth mentioning:</p>
<h3 id="google-io-annual-conference-app">🎤 Google I/O Annual Conference App</h3>
<p>Since 2011 the Google I/O companion application has been the spotlight application by Google that showcases the latest Android features. It recently incorporated Android Wear and Auto variants too.</p>
<p><a target="_blank" href="https://github.com/google/iosched/issues/309">Historically</a> the source code of the app is released 3–5 months after the conference. Still, it is a great complete application for studying.</p>
<ul>
<li><p><strong>Key Features:</strong> Multiform factor (Mobile, Tablet, Wear OS, Auto, TV), Firebase, WorkManager, Dark Mode (2019)</p>
</li>
<li><p><strong>Language:</strong> Kotlin (2018-2019+), Java (2011–2017)</p>
</li>
<li><p><strong>Architecture:</strong> MVVM (2019)</p>
</li>
<li><p><strong>Dependency Injection:</strong> Dagger Hilt</p>
</li>
<li><p><strong>Navigation:</strong> Jetpack Navigation (2019)</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit and Espresso</p>
</li>
<li><p><strong>Source:</strong> <a target="_blank" href="https://github.com/google/iosched">https://github.com/google/iosched</a></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534594390/jS8M9rKc8.png" alt="Google I/O 2019 — Demo Screenshots" /><em>Google I/O 2019 — Demo Screenshots</em></p>
<h3 id="tivi-track-show-tracking">📺 Tivi — Track Show Tracking</h3>
<p>Tivi app is currently in the early stages developed by <a target="_blank" href="https://twitter.com/chrisbanes">Chris Banes</a>, a member of the Android Developer Relations team. He closely works with <a target="_blank" href="https://twitter.com/crafty">Nick Butcher</a>, author of Plaid.</p>
<p>The app uses all the latest libraries and recommendations from Google. This app has a very modular structure, almost everything is divided into modules (about 29 modules as of writing). This app will be a good study material when the app goes into the beta stage. Now, it’s too early to recommend as main study material.</p>
<ul>
<li><p><strong>Key Features:</strong> Dark Mode, Multi-Module, Multi-Service (Trakt, TMDb), Room (Database), RxJava 2</p>
</li>
<li><p><strong>Language:</strong> Kotlin</p>
</li>
<li><p><strong>Architecture:</strong> MVVM + Lifecycle + LiveData</p>
</li>
<li><p><strong>Dependency Injection:</strong> Dagger Hilt</p>
</li>
<li><p><strong>Navigation:</strong> Jetpack Navigation</p>
</li>
<li><p><strong>Unit Tests:</strong> JUnit (Under active development)</p>
</li>
<li><p><strong>Source:</strong> <a target="_blank" href="https://github.com/chrisbanes/tivi">https://github.com/chrisbanes/tivi</a></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626534596912/nARGWX4BY.png" alt="Tivi — Demo Screenshots" /><em>Tivi — Demo Screenshots</em></p>
<p>I will continue to extend this article to include more community projects. Feel free to share projects that are ideal for learning. Thanks 🙏</p>
]]></content:encoded></item><item><title><![CDATA[Clickable link text for Android TextView — Kotlin Extension]]></title><description><![CDATA[Recently I have had to create UI that required user tappable/clickable text in the same text view. I know this is kind of unusual as the touch target for the view will likely be smaller compared to a button with no outline style. However, I wanted to...]]></description><link>https://blog.hossain.dev/clickable-link-text-for-android-textview-kotlin-extension-a36b9e03180b</link><guid isPermaLink="true">https://blog.hossain.dev/clickable-link-text-for-android-textview-kotlin-extension-a36b9e03180b</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Thu, 02 Jan 2020 00:53:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259362196/29a82347-511b-4709-8e53-8e3512df3837.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I have had to create UI that required user tappable/clickable text in the same text view. I know this is kind of unusual as the touch target for the view will likely be smaller compared to a button with no outline style. However, I wanted to share a quick Kotlin extension function that is dynamic and well tested.</p>
<p>Android TextView where “Register Now” is a tappable link with a callback.</p>
<p>Here is an example usage for generating clickable link within the same <code>TextView</code></p>
<p>So, if your project requires something like this take a look at the following extension function for <code>android.widget.TextView</code>.</p>
<p>You should be able to easily convert this to Java if needed. Let me know if you find this useful. Cheers ✌️</p>
]]></content:encoded></item><item><title><![CDATA[How to be an Android Developer with just 7 taps. Deep Dive 🔎]]></title><description><![CDATA[DISCLAIMER: This is a non-technical just-for-fun post!

In the past few years, the Android ecosystem has exploded with lots of tools, libraries, and architecture guidelines. Recently, with I/O 2017 announcement of Kotlin support, it just added anothe...]]></description><link>https://blog.hossain.dev/how-to-be-an-android-developer-with-just-7-taps-deep-dive-8abebfc07061</link><guid isPermaLink="true">https://blog.hossain.dev/how-to-be-an-android-developer-with-just-7-taps-deep-dive-8abebfc07061</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Tue, 21 May 2019 00:24:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259364727/2feb292a-4ae2-4a2c-b46b-46a106656605.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>DISCLAIMER: This is a non-technical just-for-fun post!</p>
</blockquote>
<p>In the past few years, the Android ecosystem has exploded with lots of tools, libraries, and architecture guidelines. Recently, with I/O 2017 announcement of Kotlin support, it just added another dimension.</p>
<p>Today, I am going to show you quickest way of becoming an Android Developer regardless of language and plethora of libraries, all you need is a physical Android device.</p>
<h4 id="heading-becoming-android-developer">Becoming Android developer</h4>
<p>On your device, go to <code>Settings &gt; About</code> and find “<strong>Build Number</strong>”</p>
<blockquote>
<p>p.s. The “Build Number” may be burried under additional sub section based on device manufacturer like LG, Samsung, HTC and so on. You just need to find it 🤓.</p>
</blockquote>
<p>Now, all you need to do is keep tapping the build number value until it says you have become a developer. And that’s it!</p>
<p>Here is how I became a developer using my Pixel 2 XL device:</p>
<iframe src="https://www.youtube.com/embed/JSNRZhzMsLU" width="700" height="393"></iframe>

<p>A short clip showcasing how to enable ‘Developer options’</p>
<p>If you made this far, you know this is a <em>joke</em> 🙈! By now, every Android devs know how to activate the developer options on any Android device.</p>
<p>However, back in the days, there was no tapping required to activate it, it was always there. So, when they added this feature, I’ll be honest, I did have to Google for it 😁.</p>
<h4 id="heading-under-the-hood">Under the hood</h4>
<p>As I was curious, I wanted to see how the logic works in the Android settings screen. I found that all these logic of activating the developer options is in <a target="_blank" href="https://android.googlesource.com/platform/packages/apps/Settings/+/master/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java"><strong>BuildNumberPreferenceController.java</strong></a> (<em>AOSP</em>) source code.</p>
<p>First, the most important constant that defines how many times we have to tap to become the ‘developer’ is:</p>
<p>static final int <strong>TAPS_TO_BE_A_DEVELOPER</strong> = <strong>7</strong>;</p>
<p>Why 7? Maybe because it’s considered a lucky number? ¯\_(ツ)_/¯</p>
<h4 id="heading-pre-conditions">Pre-conditions</h4>
<p>Here some of the pre-conditions that have to be met before developer settings can be activated by tapping 7 times on the build number.</p>
<ul>
<li>Admin user of the device (using <a target="_blank" href="https://developer.android.com/reference/android/os/UserManager.html">UserManager</a>)</li>
<li>Device Provisioning is complete</li>
<li>Tapping is not done by <a target="_blank" href="https://developer.android.com/studio/test/monkeyrunner">monkey-runner</a></li>
<li>Debugging feature is not disabled by Device Owner/Work Profile</li>
</ul>
<p>After these requirements are met, all there is left to do is count-down the number of taps. Here is a <em>simplified</em> code snapshot with some added inline comments:</p>
<p>if (mDevHitCountdown &gt; 0) {<br />    mDevHitCountdown--;<br />    if (mDevHitCountdown == 0 &amp;&amp; !mProcessingLastDevHit) {<br />        // Open the lock screen validation screen before activating<br />        mProcessingLastDevHit = helper.launchConfirmationActivity();<br />        if (!mProcessingLastDevHit) {<br />            // Activates developer settings after lock verification<br />            enableDevelopmentSettings();<br />        }<br />    } else if (mDevHitCountdown &gt; 0<br />            &amp;&amp; mDevHitCountdown &lt; (TAPS_TO_BE_A_DEVELOPER - 2)) {  </p>
<p>        // Show - "You are X taps away from being developer."<br />        mDevHitToast = Toast.makeText(getQuantityString(<br />                        R.plurals.show_dev_countdown,<br />                        mDevHitCountdown,<br />                        mDevHitCountdown),<br />                Toast.LENGTH_SHORT);<br />        mDevHitToast.show();<br />    }<br />} else if (mDevHitCountdown &lt; 0) {<br />    // This means, you have already tapped 7 times, you're a DEV!<br />    mDevHitToast = Toast.makeText(R.string.show_dev_already,<br />            Toast.LENGTH_LONG);<br />    mDevHitToast.show();<br />}</p>
<p>That’s it, mystery solved 🎉</p>
]]></content:encoded></item><item><title><![CDATA[Using custom domain for GitHub pages]]></title><description><![CDATA[Recently I decided to host my personal portfolio site using GitHub pages. Even though they have very detailed instruction on how to setup a custom domain, I found it cumbersome to get to the right information. 🙄
Here are 2 key steps to setup your Gi...]]></description><link>https://blog.hossain.dev/using-custom-domain-for-github-pages-86b303d3918a</link><guid isPermaLink="true">https://blog.hossain.dev/using-custom-domain-for-github-pages-86b303d3918a</guid><dc:creator><![CDATA[Hossain Khan]]></dc:creator><pubDate>Fri, 19 Jan 2018 03:02:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259369479/03a0c4a9-624f-4b19-8f6c-9de3611b3c8d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I decided to host my personal portfolio site using <a target="_blank" href="https://pages.github.com/">GitHub pages</a>. Even though they have very detailed <a target="_blank" href="https://help.github.com/articles/using-a-custom-domain-with-github-pages/">instruction</a> on how to setup a custom domain, I found it cumbersome to get to the right information. 🙄</p>
<p>Here are <strong>2</strong> key steps to setup your GitHub pages <a target="_blank" href="https://guides.github.com/features/pages/">enabled</a> project to use your <strong>custom domain</strong>.</p>
<h4 id="heading-step-1-set-domain-in-github-project">⚙️Step 1 — Set domain in GitHub project</h4>
<p>Go to your GitHub Pages site’s repository settings. Under “Custom domain”, add or remove your custom domain and click “Save”.</p>
<p>Setting “custom domain” creates a file named <code>**CNAME**</code> in the same repository. Don’t delete it.</p>
<h4 id="heading-step-2-set-custom-resource-record-for-domain">⚙️Step 2 — Set custom resource record for domain</h4>
<p>This step is specific to your domain name register (like GoDaddy, Domain.com, Google Domains, etc). All you need to do is set <code>**A**</code> &amp; <code>**CNAME**</code> records for the selected domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709259367971/642cf5d4-7ad9-4021-8ceb-e27389b020c4.png" alt /></p>
<p>This is a sample screenshot taken from the <strong>Google Domains</strong> portal.</p>
<p>For <code>**A**</code> record, set <code>185.199.108.153</code>, <code>185.199.109.153</code>, <code>185.199.110.153</code> and <code>185.199.111.153</code>. To redirect <code>**www**</code> subdomain to the original domain, add a <code>**CNAME**</code> record with your GitHub pages profile URL with a <code>.</code>(dot) in the end, for example, ‘<code>*YOUR-GITHUB-USERNAME.github.io.*</code>’.</p>
<p><strong><em>Official References*</em></strong>: For most up to date IP Addresses, use GitHub’s<em> [</em>official documentation<em>](https://help.github.com/articles/setting-up-an-apex-domain/) </em>and for setting up CNAME use this<em> [</em>documentation<em>](https://help.github.com/articles/setting-up-a-www-subdomain/)</em>.*</p>
<p>That’s it, both <code>www.your-domain.com</code> and <code>your-domain.com</code> will now go to your selected GitHub pages site <em>(may need to wait up to 24 hours)</em>. If you want to see a live example, you may visit my portfolio “<a target="_blank" href="http://hossainkhan.com/">hossainkhan.com</a>” hosted via GitHub <a target="_blank" href="https://github.com/amardeshbd/hossainkhan.com">pages</a> repository ✌️.</p>
<blockquote>
<p>UPDATE #1: The IP addresses for DNS <code>A</code> record is updated. The new IP addresses are required to use the free HTTPS support for GitHub pages.</p>
<p>UPDATE #2: Some people said this change is not working, it is actually because the DNS update can <strong>take upto</strong> <strong>24 hours</strong> to propagate. So, I guess try hitting your domain next day 🤓</p>
<p>NOTE #1: Even though it’s very obvious, you should replace <code>*YOUR-GITHUB-USERNAME*</code> and <code>your-domain.com</code> with your personal github username and domain name you are trying to use respectively.</p>
</blockquote>
]]></content:encoded></item></channel></rss>