<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts // AJ Kueterman</title><description/><link>https://ajkueterman.com</link><item><title>A Little More Prompting</title><link>https://ajkueterman.com/posts/more-gemini-nano-prompting</link><guid isPermaLink="true">https://ajkueterman.com/posts/more-gemini-nano-prompting</guid><description>Diving deeper into the ML Kit GenAI Prompt API&apos;s new features - system instructions and structured output.</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve spent a little more time building with the ML Kit GenAI &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android&quot;&gt;Prompt API&lt;/a&gt; on Android, which has continued to evolve even since I started playing around with it earlier this summer.&lt;/p&gt;
&lt;p&gt;Two notable pieces I wanted to touch on were &lt;a href=&quot;#system-instructions&quot;&gt;system instructions&lt;/a&gt; and &lt;a href=&quot;#structured-output&quot;&gt;structured output&lt;/a&gt;. Each is a tool for engineering better prompts and building more specific user experiences.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Both system instructions and structured output were added in &lt;code&gt;com.google.mlkit.genai-prompt:1.0.0-beta3&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;System Instructions&lt;/h2&gt;
&lt;p&gt;System instructions help you shape the output &amp;amp; tone of your prompt responses. You can define a style, format, or constraints that are consistent across your dynamic queries.&lt;/p&gt;
&lt;p&gt;In my &lt;a href=&quot;/posts/on-device-ai-with-gemini-nano&quot;&gt;D&amp;amp;D Assistant app&lt;/a&gt; I added system instructions to help guide my responses in both tone and format.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val SYSTEM_INSTRUCTION = &quot;&quot;&quot;  
    You are a Dungeon Master&apos;s assistant inside a D&amp;amp;D 5e SRD reference app.
    Every message you receive is one SRD entry. 
    Reply with exactly ONE sentence of at most 30 words, and nothing else.
    Lead with the single most important fact a DM would want at a glance, favoring hard numbers and mechanics:
    damage, AC, HP, saves, DCs, ranges, durations, uses. 
    Never open with &quot;This&quot; or a restatement of the title. 
    No preamble, no markdown, no second sentence — output the sentence alone.
&quot;&quot;&quot;.trimIndent()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then used this instruction when generating content with a dynamic prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val prompt = Prompt.userContent(entry) // my dynamically generated prompt
val request = generateContentRequest(  
    systemInstruction = SystemInstruction(Prompt.SYSTEM_INSTRUCTION),  
    text = TextPart(prompt),  
) {  
    temperature = 0.2f  
    candidateCount = 1  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helped guide my prompts in a repeatable way so I could get consistent output across many different rules entries I needed to summarize.&lt;/p&gt;
&lt;p&gt;Remember to keep system instructions brief &amp;amp; direct. They should be under 100 tokens and clearly state your persona, output shape, or constraints.&lt;/p&gt;
&lt;p&gt;If your use case is complex enough that you&apos;re reaching for the Prompt API over one of the other feature-specific APIs like Summarization, chances are you&apos;ll want to define your output in a repeatable way. System instructions should be a core part of that strategy.&lt;/p&gt;
&lt;h3&gt;Prefix Caching&lt;/h3&gt;
&lt;p&gt;If you&apos;ve done some digging into this API you may also have seen the concept of &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android/prefix-caching&quot;&gt;prefix caching&lt;/a&gt;, which allows you to define a re-usable prefix for your prompts that get cached for future queries. This is primarily a performance feature, designed to handle large prompt instruction prefixes.&lt;/p&gt;
&lt;p&gt;The guidance is to not use system instructions with prefix caching, and instead decide on the right approach for your prompt workflow. Use prefix caching over system instructions when you have to repeat a large (500+ token) prefix on each prompt to optimize for performance.&lt;/p&gt;
&lt;h2&gt;Structured Output&lt;/h2&gt;
&lt;p&gt;Text output is great, but the Prompt API is capable of generating all sorts of text, including &lt;em&gt;structured&lt;/em&gt; text - like JSON. This is a super powerful aspect of LLMs that enable them to do all sorts of novel things, and is available on device as well via this API.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android/structured-output&quot;&gt;structured output&lt;/a&gt; API allows you to define an output class for a given prompt. You can define your object structure, including hints for the model, and then provide that along with your prompt.&lt;/p&gt;
&lt;p&gt;Let&apos;s &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android/structured-output#define_the_output_structure&quot;&gt;define a simple object structure&lt;/a&gt; that could be used to convert a monster entry into a summary data model for my summary card.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide

@Generable(&quot;Top level information about a D&amp;amp;D monster&quot;)
data class Monster(
    @Guide(description = &quot;The common name of the monster&quot;)
    val name: String,
    
    @Guide(description = &quot;The monster type, like undead, fiend, or humanoid.&quot;)
    val type: String,
    
    @Guide(
     description = &quot;The monster total HP or hit points.&quot;,
     minimum = 0.0,
    )
    val hitPoints: Int,
    
    @Guide(description = &quot;The monster AC or armor class rating.&quot;)
    val armorClass: Int,
    
    @Guide(description = &quot;The monster&apos;s initiative modifier.&quot;)
    val initiative: Int,
    
    @Guide(description = &quot;An optional list of immunities like Acid, Prone, etc.&quot;)
    val immunities: List&amp;lt;String&amp;gt;?,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you can define this output model when prompting. The &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android/structured-output#define_the_output_structure&quot;&gt;supported types/constraints&lt;/a&gt; cover object types you&apos;d typically deal with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val promptText = &quot;&amp;lt;Monster block text&amp;gt;&quot;
val baseRequest = GenerateContentRequest.Builder(
	text = TextPart(promptText)
).build()
val typedRequest = generateTypedContentRequest(
    generateContentRequest = baseRequest,
    outputClass = Monster::class, // Define the output model
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is a populated &lt;code&gt;Monster&lt;/code&gt; instance, fully populated by the model!&lt;/p&gt;
&lt;p&gt;You can do a lot with structured JSON objects. The possibilities are endless for inputting information with well-built prompts, allowing an LLM to process / expand upon them, and generate output models that your app can understand.&lt;/p&gt;
&lt;p&gt;Maybe the &lt;code&gt;outputClass&lt;/code&gt; is a data model tuned as state for your UI, so that users can input queries to the LLM (via text or app interaction) and the app responds by updating its UI? Ask a question, render a button. I think this could be really powerful!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Overall I&apos;m so excited about the possibility of on-device AI. It makes this technology much more affordable and accessible for smaller apps like the ones I enjoy building.&lt;/p&gt;
&lt;p&gt;The Prompt API feels like the true star of the ML Kit GenAI APIs. The features being added continue to support more specific workflows, while giving devs more flexibility on model output. I encourage you to dive deeper and let me know what you&apos;re building on &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;Mastodon&lt;/a&gt; or &lt;a href=&quot;https://bsky.app/profile/ajkueterman.com&quot;&gt;Bluesky&lt;/a&gt;!&lt;/p&gt;
</content:encoded></item><item><title>Building a D&amp;D Assistant with On-Device AI on Android</title><link>https://ajkueterman.com/posts/on-device-ai-with-gemini-nano</link><guid isPermaLink="true">https://ajkueterman.com/posts/on-device-ai-with-gemini-nano</guid><description>Harnessing the power of Gemma 4 via the Nano 4 APIs on Android to build a D&amp;D assistant for the gaming table.</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Earlier this year, Google announced &lt;a href=&quot;https://developer.android.com/blog/posts/announcing-gemma-4-in-the-ai-core-developer-preview&quot;&gt;Gemini Nano 4 is available as a Preview release via the AI Core Beta&lt;/a&gt;. This means developers can start building for Nano 4 to provide fully on-device AI experiences for their Android apps.&lt;/p&gt;
&lt;p&gt;As Gemini Nano advances and starts to add more and more powerful models, I&apos;ve been asking myself how I could harness this on-device advantage to deliver unique experiences.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Gemma vs. Nano
&lt;a href=&quot;https://blog.google/innovation-and-ai/technology/developers-tools/gemma-4/&quot;&gt;Gemma 4&lt;/a&gt; is Google&apos;s latest iteration of &lt;a href=&quot;https://deepmind.google/models/gemma/&quot;&gt;Gemma&lt;/a&gt;, its powerful open model. &lt;a href=&quot;https://developer.android.com/ai/gemini-nano&quot;&gt;Nano&lt;/a&gt; packages the small variants of these models into &lt;a href=&quot;https://developers.google.com/ml-kit/genai/aicore-dev-preview&quot;&gt;AICore&lt;/a&gt; for use on Android devices.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The DM&apos;s Assistant&lt;/h2&gt;
&lt;p&gt;I love playing tabletop roleplaying games. Mostly Dungeons &amp;amp; Dragons 5e. Ever since I got into the hobby, I&apos;ve mostly been a dungeon master.[^1] The DM is the one charged with guiding the story and adjudicating the rules of the game.&lt;/p&gt;
&lt;p&gt;As a DM, one of the hardest things to manage is having the right rules on-hand at the right time. Players are always going to try to grapple the Giant Spider at the weirdest moments. Usually this means either quickly googling rules or flipping through a rulebook.&lt;/p&gt;
&lt;p&gt;I&apos;ve always wished I could have an assistant sitting next to me at our game listening to the flow of play and making sure the right information was always at my fingertips. The major challenge being that D&amp;amp;D happens in person or through a huge array of digital tools, and few DMs have the same setup. You could probably integrate with Roll20 to follow the flow of the game, but then you&apos;d have to do something different for campaigns on D&amp;amp;D Beyond or when your group all meets in person.&lt;/p&gt;
&lt;p&gt;If your phone could listen to you (and, optionally, your players) talk through the flow of the game, there may be a way to reason about what rules you need to see when. On-device ML/AI would make listening easier, and summarization could make the information glanceable so that you&apos;d never have to take your focus off the roleplaying.&lt;/p&gt;
&lt;p&gt;So I decided to see how I could apply these (now more powerful) Nano-based APIs to this fun problem.&lt;/p&gt;
&lt;h2&gt;Nano APIs&lt;/h2&gt;
&lt;p&gt;First I got a bit more familiar with what was possible with Nano 4. For Android developers, access to this model is available via the &lt;a href=&quot;https://developer.android.com/ai/gemini-nano#ml_kit_genai_apis&quot;&gt;&lt;strong&gt;ML Kit GenAI APIs&lt;/strong&gt;&lt;/a&gt;...I&apos;ll refer to these as the &quot;Nano APIs&quot; moving forward.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.android.com/ai/gemini-nano#key-features&quot;&gt;key features&lt;/a&gt; of these APIs are text-based processing like Summarization / Proofreading / Rewriting, Image Descriptions and Speech Recognition. There is also a more general &apos;Prompt&apos; API for generating text content from text-based multimodal prompting, which has some powerful implications for prompt engineering.&lt;/p&gt;
&lt;p&gt;Based on this list, I decided using a combination of Speech Recognition and Summarization might enable me to build something that has been on my wish list for a while now.&lt;/p&gt;
&lt;h2&gt;Revelations &amp;amp; Limitations&lt;/h2&gt;
&lt;p&gt;After some initial experimentation, I quickly learned a few things that would shape this project.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All of the Nano APIs &lt;a href=&quot;https://developer.android.com/ai/gemini-nano#architecture&quot;&gt;use the same underlying model running inside AICore on the Android OS&lt;/a&gt;. This means they are competing for requests, and the API enforces limits on your app. If you want to use Nano 4-based voice recognition while also using it to summarize, you&apos;re quickly going to be throttled.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://developers.google.com/ml-kit/genai/summarization/android&quot;&gt;Summarization API&lt;/a&gt; is very strict about its output. It summarizes text as a bulleted list. This restriction, plus some restrictions on how you send summarization requests, made me look to the more flexible solution - prompting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The impact on this demo was straightforward. Drop the Nano-based speech recognition pipeline and use the more general Prompt API to generate glanceable rules.&lt;/p&gt;
&lt;h2&gt;ML Kit / Nano Combined Pipeline&lt;/h2&gt;
&lt;p&gt;Here are the core pieces of the app&apos;s processing pipeline, from the DM to relevant rules summaries, with my specific caveats called out.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#speech-recognition&quot;&gt;Speech Recognition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rules-lookup&quot;&gt;Rules Lookup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#custom-summarization&quot;&gt;Custom Summarization&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Speech Recognition&lt;/h3&gt;
&lt;p&gt;The GenAI Speech Recognition API supports both &lt;strong&gt;Basic&lt;/strong&gt; and &lt;strong&gt;Advanced&lt;/strong&gt; modes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced mode provides similar functionality to Basic, but its use of Gemini Nano gives broader language coverage and natural language reasoning.&lt;/li&gt;
&lt;li&gt;Advanced mode only runs on Pixel 10 devices (with more in development), while Basic runs on most Android devices with API level 31 and higher.&lt;/li&gt;
&lt;li&gt;Because it uses AICore, Advanced mode is subject to the model bottleneck issue when put in contention with my summary requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these callouts in mind, I decided &lt;strong&gt;Basic&lt;/strong&gt; would be good enough for my needs in this case. We run speech recognition in a loop, capturing every utterance to be used as keywords in our rules search.&lt;/p&gt;
&lt;h3&gt;Rules Lookup&lt;/h3&gt;
&lt;p&gt;As mentioned before, a lot of the complexity of this app comes from the actual lookup of search terms in the D&amp;amp;D rules database.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] SRD Database
A piece of the app that I don&apos;t go into detail here is the SRD[^2] &apos;ingest&apos; functionality. The app downloads a copy of the &lt;a href=&quot;https://github.com/5e-bits/5e-database#how-to-run&quot;&gt;5e rules database&lt;/a&gt; at build time to build its local SQLite DB of rules entries.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As voice input is converted into text, this text is then converted into search terms used to query this D&amp;amp;D database of rules. This logic mostly lives in my &lt;code&gt;TopicExtractor&lt;/code&gt; class, which processes each utterance - &quot;&lt;em&gt;the goblin ducks behind cover&lt;/em&gt;&quot; - and cleans it into search terms to query the database with.&lt;/p&gt;
&lt;p&gt;The actual DB search uses &lt;a href=&quot;https://www.sqlite.org/fts5.html&quot;&gt;Full Text Search 5&lt;/a&gt; with the built in &lt;a href=&quot;https://emschwartz.me/understanding-the-bm25-full-text-search-algorithm/&quot;&gt;&lt;code&gt;bm25&lt;/code&gt;&lt;/a&gt; function to properly search &amp;amp; rank SRD entry hits. Lists of hits are de-duped before rendering them to the screen, so the user isn&apos;t spammed with duplicate rules entries as they talk through the game.&lt;/p&gt;
&lt;p&gt;I mention this section specifically because just the combination of Basic Speech Recognition and a powerful SRD database search delivers a ton of value. The final piece, which actually utilizes AICore and Nano 4, is like the last 10% polish on the idea.&lt;/p&gt;
&lt;h3&gt;Custom Summarization&lt;/h3&gt;
&lt;p&gt;As mentioned earlier, the &lt;a href=&quot;https://developers.google.com/ml-kit/genai/summarization/android&quot;&gt;Summarization API&lt;/a&gt; is very specific about its input and output. Pass in raw text content, and get out 2-3 bullet points in summary.&lt;/p&gt;
&lt;p&gt;For the case of my summary text, I wanted to have a 1-2 sentence summary in prose as the output. Maybe not a huge difference by itself.&lt;/p&gt;
&lt;p&gt;However, I also am dealing with a slightly different type of content as input. Depending on what type of SRD rules entry I get back, I may want to add that as context to generate the summary text. This added flexibility is something that doesn&apos;t exist within the Summarization API, but is offered with the &lt;a href=&quot;https://developers.google.com/ml-kit/genai/prompt/android&quot;&gt;Prompt API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s a real example from the demo app, where &lt;code&gt;typeHint&lt;/code&gt; is something like &lt;code&gt;&quot;spell&quot;&lt;/code&gt;, &lt;code&gt;&quot;monster stat block&quot;&lt;/code&gt;, or &lt;code&gt;&quot;condition&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val prompt = 
  &quot;&quot;&quot;  
  You are a Dungeon Master&apos;s assistant. 
  Summarize the following D&amp;amp;D 5e SRD $typeHint in ONE sentence (max 30 words). 
  Lead with the single most important fact a DM would want at a glance. 
  Do not preface with phrases like &quot;This $typeHint…&quot;; start with the substance.  
  
  Title: ${entry.name}  
  Body: $body  
  &quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps tailor our output to exactly what we want to see for each summary.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Device Support
Prompting is available on the same devices that Summarization is available for - usually the latest flagships +1 generation - but which version of Nano it uses depends on the device. Pixel 9 gets Nano 2 (or Nano 3 Preview), and Pixel 10 gets Nano 3 (or Nano 4 Preview) and so on.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once we define our prompt, we use the ML Kit GenAI API to get a &lt;code&gt;GenerativeModel&lt;/code&gt; of our choosing. Here we are specifying that we want to use the &lt;code&gt;PREVIEW&lt;/code&gt; version of the model (to get us access to Nano 4 on our Pixel 10) and that we want to use the &lt;code&gt;FULL&lt;/code&gt; version (as opposed to &lt;code&gt;FAST&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private val model: GenerativeModel = Generation.getClient(  
    generationConfig {  
        modelConfig = ModelConfig.builder()  
            .apply {  
                releaseStage = ModelReleaseStage.PREVIEW  
                preference = ModelPreference.FULL  
            }  
            .build()  
    },  
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can use this &lt;code&gt;GenerativeModel&lt;/code&gt; directly to generate a text response based on our prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val response = model.generateContent(prompt)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual response is a list of candidate text content. Each response type is a piece of output text and a &apos;finish reason&apos; - like &lt;code&gt;STOP&lt;/code&gt; or &lt;code&gt;MAX_TOKENS&lt;/code&gt;. In our case we&apos;ll use the first result and only inspect the finish reason if there is no valid text.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val text = response.candidates.firstOrNull()?.text?.trim().orEmpty()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is really all there is to prompting at its core - input text via Prompt and get text content back. The magic happens inside the LLM, and there&apos;s a ton you can do with good prompts.&lt;/p&gt;
&lt;p&gt;Here&apos;s the full code for my implementation of the &lt;code&gt;summarize&lt;/code&gt; function from my &lt;code&gt;GenerativeBackend&lt;/code&gt;. You can see here that there is some logic to handle serializing calls to &lt;code&gt;generateContent&lt;/code&gt;, as my app may have many calls to summarize text happening in quick succession.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Serializes every inference: the single AICore runtime rejects overlapping  
// generateContent calls with RESPONSE_PROCESSING_ERROR.
private val inferenceMutex = Mutex()  
  
override suspend fun summarize(entry: SrdEntry): String = withContext(Dispatchers.IO) {
    prepare() // Check model availability status &amp;amp; call `model.warmup()`
    val prompt = Prompt.summarize(entry)
    val response = inferenceMutex.withLock { model.generateContent(prompt) }  
    val text = response.candidates.firstOrNull()?.text?.trim().orEmpty()  
    if (text.isEmpty()) {  
        val reason = response.candidates.firstOrNull()?.finishReason  
        throw IOException(&quot;Prompt returned no text for ${entry.name} (finishReason=$reason)&quot;)  
    }
    text
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The View Model managing my app&apos;s timeline UI manages calling my &lt;code&gt;GenerativeBackend&lt;/code&gt; for each new card to summarize its rule content as they populate. The result is a constantly updated list of rules ready to glance at as we need them.&lt;/p&gt;
&lt;h2&gt;Demo App&lt;/h2&gt;
&lt;p&gt;Just to give you a working idea of the semi-finished product, here&apos;s a demo of the app handling the following prompt.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Suddenly, a &lt;em&gt;goblin&lt;/em&gt; jumps out and attempts to &lt;em&gt;grapple&lt;/em&gt; you. You spot another goblin behind &lt;em&gt;cover&lt;/em&gt; in the bushes, holding a &lt;em&gt;hand crossbow&lt;/em&gt;. What do you do?”&lt;/p&gt;
&lt;p&gt;“I cast &lt;em&gt;fireball&lt;/em&gt;!”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div class=&quot;demo-short&quot;&amp;gt;
&amp;lt;div class=&quot;demo-short-dither&quot;&amp;gt;
&amp;lt;i style=&quot;--dot-gap: 15px; --dot-rx: 40%; --dot-ry: 50%;&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;video controls playsinline preload=&quot;metadata&quot; width=&quot;500&quot;&amp;gt;
&amp;lt;source src=&quot;/video/dnd-intern-demo-001.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.demo-short {
/* NOTE admonition blue, a shade deeper in light mode (blue-700 / blue-400). */
--demo-short-glow: 29 78 216;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position: relative;
display: flex;
justify-content: center;
padding: 3rem 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/*&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 1-bit style radial burst: every dot is drawn at full opacity, so the&lt;/li&gt;
&lt;li&gt;fade comes from the dot grid getting sparser as it reaches further out,&lt;/li&gt;
&lt;li&gt;not from transparency.
&lt;em&gt;/
.demo-short-dither {
position: absolute;
top: 0;
bottom: 0;
/&lt;/em&gt; Bleed past max-w-prose; the layout&apos;s overflow-x-hidden clips any spill. */
left: 50%;
width: 160%;
transform: translateX(-50%);
}&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.demo-short-dither i {
position: absolute;
inset: 0;
background-image: radial-gradient(
circle at center,
rgb(var(--demo-short-glow)) 0.5px,
transparent 0.5px
);
background-size: var(--dot-gap) var(--dot-gap);
/* Hard-edged cutoff keeps the grid 1-bit: a dot is either on or off. */
-webkit-mask-image: radial-gradient(
ellipse var(--dot-rx) var(--dot-ry) at 50% 50%,
#000 99%,
transparent 100%
);
mask-image: radial-gradient(
ellipse var(--dot-rx) var(--dot-ry) at 50% 50%,
#000 99%,
transparent 100%
);
}&lt;/p&gt;
&lt;p&gt;.demo-short video {
position: relative;
max-width: 100%;
border-radius: 12px;
}&lt;/p&gt;
&lt;p&gt;.dark .demo-short {
--demo-short-glow: 96 165 250;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&apos;m really happy with how this app turned out! I&apos;ve been playing with it for awhile and I feel like it does a good job, particularly with &apos;Rules Glossary&apos; type lookups that are most often the &apos;edge-cases&apos; in a game.&lt;/p&gt;
&lt;p&gt;Hopefully this gives you some inspiration to build your own Android experiences with Nano 4 on-device AI. The ability to send open-ended prompts gives you a ton of flexibility to tailor your LLM use to your app&apos;s specific needs.&lt;/p&gt;
&lt;p&gt;Thoughts on my approach? Questions? Suggestions for what to build next? Let me know on &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;Mastodon&lt;/a&gt; or &lt;a href=&quot;https://bsky.app/profile/ajkueterman.com&quot;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^1]: I found that it was a lot easier to get a group together if you offer to DM.&lt;/p&gt;
&lt;p&gt;[^2]: SRD stands for &apos;System Reference Document&apos;, which is the official rules document for D&amp;amp;D 5th edition licensed under the Creative Commons.&lt;/p&gt;
</content:encoded></item><item><title>Dark Sun 2027 is... Gross.</title><link>https://ajkueterman.com/posts/dark-sun-2027-is-gross</link><guid isPermaLink="true">https://ajkueterman.com/posts/dark-sun-2027-is-gross</guid><description>The regurgitation of D&amp;D&apos;s long-lost &apos;Dark Sun&apos; setting gets a new, gory, gross edition for 5.5e in 2027.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I just wanted to state for the record that the new Dark Sun setting for D&amp;amp;D 5.5e is gross. From the tone of the &lt;a href=&quot;https://youtu.be/9CtTscZmdw0?si=23WrO0pbzX3Rwsl_&quot;&gt;announcement trailer&lt;/a&gt;, and subsequent art/cover releases, it&apos;s violent, gory, and mature. &lt;em&gt;That doesn&apos;t make it cool.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.polygon.com/dnd-dark-sun-dungeons-dragons-return/&quot;&gt;Announced just this week at Gen Con&lt;/a&gt;, D&amp;amp;D is bringing back the Dark Sun campaign setting first released 35 years ago in 1991. Dark Sun was a post-apocalyptic &apos;Mad Max&apos;-style world where there are no gods, magic is inherently destructive, and life in these wastes is brutal, cruel, and desolate. Already some kind of edgelord-y stuff there.&lt;/p&gt;
&lt;p&gt;But in the original setting there was a nugget of something more fun and familiar. The art of &lt;a href=&quot;https://en.wikipedia.org/wiki/Gerald_Brom&quot;&gt;Gerald Brom&lt;/a&gt; gave everything a bit more of a sense of weirdness, fantasy and...sexiness? (in that 90s sort-of way). It feels like a mix of gritty realism with some sci-fi fantasy layered on.&lt;/p&gt;
&lt;p&gt;Just look at some of the art from this &apos;fantasy Mos Eisley&apos; era of Dark Sun:&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;dark-sun-gallery&quot;&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/old/Reckoneers.jpg&quot; alt=&quot;Original Dark Sun art&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/old/br_71.jpg&quot; alt=&quot;Original Dark Sun art&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/old/DuneTrader.jpg&quot; alt=&quot;Original Dark Sun art&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;And compare that to the &apos;rivers of blood&apos;, Doom-esque reincarnation in the 2027 art:&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;dark-sun-gallery&quot;&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/new/dnd-darksun_tyr_maihope.jpeg&quot; alt=&quot;2027 Dark Sun art&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/new/dnd-darksun_raam_lius_lasahido.jpeg&quot; alt=&quot;2027 Dark Sun art&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/dark-sun/new/dnd-darksun_urik_david-sarabia.jpeg&quot; alt=&quot;2027 Dark Sun art&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.dark-sun-gallery {
display: flex;
align-items: flex-end;
gap: 1rem;
overflow-x: auto;
margin: 2rem 0;
padding: 0.5rem 0;
scrollbar-width: thin;
}&lt;/p&gt;
&lt;p&gt;.dark-sun-gallery img {
display: block;
height: 300px;
width: auto;
margin: 0;
padding: 0;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
align-self: flex-end;
transition: transform 0.2s ease;
}&lt;/p&gt;
&lt;p&gt;.dark-sun-gallery img:first-child {
margin: 0;
}&lt;/p&gt;
&lt;p&gt;.dark-sun-gallery::-webkit-scrollbar {
height: 8px;
}&lt;/p&gt;
&lt;p&gt;.dark-sun-gallery::-webkit-scrollbar-track {
background: transparent;
}&lt;/p&gt;
&lt;p&gt;.dark-sun-gallery::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}&lt;/p&gt;
&lt;p&gt;.dark .dark-sun-gallery::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;Gone are the smooth mega-quads and the pointy fingernails. The desolate wastes seem to be replaced with hellish metropolises and alien destruction. Blood, dismemberment, and heavy metal are all that we are left with. So original.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Honestly I&apos;m not trying to go to bat here for the original Dark Sun setting too hard. The truth is it was problematic in more ways than one, and going back to it in 2026 (as I am coincidentally doing right now with my gaming group) requires some adaptation and recognition of the issues.&lt;/p&gt;
&lt;p&gt;That said, at least it had a point of view that felt fresh, and still does today. What we&apos;re being fed now by Hasbro is something else entirely, and feels like it&apos;ll be Dark Sun in name only. I&apos;m good.&lt;/p&gt;
&lt;p&gt;Catch me next year playing &lt;a href=&quot;https://www.theverge.com/games/973652/dnd-dungeons-dragons-world-of-warcraft-star-wars-universes-beyond&quot;&gt;Star Wars D&amp;amp;D instead&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>LitraSwitch</title><link>https://ajkueterman.com/posts/litraswitch</link><guid isPermaLink="true">https://ajkueterman.com/posts/litraswitch</guid><description>Control your Litra devices with a single click.</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some of the best software I use solves a problem that is already &quot;solved&quot; in other ways.&lt;/p&gt;
&lt;p&gt;I had a minor inconvenience in my desk setup that could be mostly solved by using existing tools. The ability to turn off/on my Logitech Litra Glow light with a click of a button.&lt;/p&gt;
&lt;p&gt;Yes, I could lean slightly forward in my chair to reach over my monitor and press the physical button. Yes I could download the bloated Logitech Options software and click thru a menu to do it. Yes I could pop open a terminal and use the awesome &lt;a href=&quot;https://github.com/timrogers/litra-rs&quot;&gt;open source CLI&lt;/a&gt; to do it, or even &lt;a href=&quot;https://github.com/timrogers/litra-autotoggle&quot;&gt;auto-toggle&lt;/a&gt; the light if I had my camera on or not.&lt;/p&gt;
&lt;p&gt;But that&apos;s not &lt;em&gt;exactly&lt;/em&gt; what I wanted. I wanted an always-available button in my menu bar that I could tap to flip on the light.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https://litraswitch.app/&quot;&gt;&lt;strong&gt;LitraSwitch&lt;/strong&gt;&lt;/a&gt; to do just that. Toggle your Logitech Litra lights with the click of a button.&lt;/p&gt;
&lt;p&gt;It allows you to toggle all of your Logitech Litra devices on/off with a button in your menu bar. Customize color temperature and brightness in preferences. More features may come - if I ever need them.&lt;/p&gt;
&lt;p&gt;If you&apos;re &lt;em&gt;exactly&lt;/em&gt; like me, and this solution helps you, check it out. If you enjoy it - let me know &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;on Mastodon&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Well Then, Who&apos;s Building It?</title><link>https://ajkueterman.com/posts/whos-building-the-thing-with-ai-then</link><guid isPermaLink="true">https://ajkueterman.com/posts/whos-building-the-thing-with-ai-then</guid><description>Musings for the software craftspeople facing a new AI age.</description><pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been in my head about the new world of &apos;agentic AI&apos;. &lt;a href=&quot;https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04&quot;&gt;Gas towns&lt;/a&gt; and &lt;a href=&quot;https://ghuntley.com/loop/&quot;&gt;Wiggums&lt;/a&gt;, &lt;a href=&quot;https://openclaw.ai/&quot;&gt;Open Claws&lt;/a&gt; and &lt;a href=&quot;https://steipete.me/posts/2026/openclaw&quot;&gt;acquisitions&lt;/a&gt;. Hacker News is ablaze with AI news / think pieces from every conceivable angle.&lt;/p&gt;
&lt;p&gt;In my corner of the world, it seems like developers are mostly on the cynical end. &lt;a href=&quot;https://nolanlawson.com/2026/02/07/we-mourn-our-craft/&quot;&gt;Mourning the death of the software engineering discipline&lt;/a&gt; and marking the beginning of the &lt;a href=&quot;https://johan.hal.se/wrote/2026/02/03/the-sideprocalypse/&quot;&gt;SaaS apocalypse&lt;/a&gt;. Calling out how &lt;a href=&quot;https://www.cnbc.com/2026/01/10/micron-ai-memory-shortage-hbm-nvidia-samsung.html&quot;&gt;AI is causing RAM shortages&lt;/a&gt; which are quickly expanding to &lt;a href=&quot;https://mashable.com/article/ai-hard-drive-hdd-shortages-western-digital-sold-out&quot;&gt;storage&lt;/a&gt;, and how this is just another way that AI companies are &lt;a href=&quot;https://infosec.exchange/@mttaggart/116076953346773167&quot;&gt;wrestling computing from our hands&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But some are sharing their journey into this new world with enthusiasm and optimism. With the help of Claude &amp;amp; Codex, indie dev veteran &lt;a href=&quot;https://mastodon.social/@stroughtonsmith&quot;&gt;Steve Troughton-Smith&lt;/a&gt; has been &lt;a href=&quot;https://mastodon.social/@stroughtonsmith/116048884643347924&quot;&gt;cranking through new apps&lt;/a&gt; seemingly by the dozen, including porting iOS apps to Android purely with prompts. I&apos;ve seem similar threads from SwiftUI pro &lt;a href=&quot;https://mastodon.social/@simonbs&quot;&gt;Simon B. Støvring&lt;/a&gt; with &lt;a href=&quot;https://mastodon.social/@simonbs/116068142802832870&quot;&gt;caveats&lt;/a&gt;. Just to name a few.&lt;/p&gt;
&lt;p&gt;In my own experience, Claude has helped me ship updates to both &lt;a href=&quot;https://apps.apple.com/us/app/octonote/id1433164731&quot;&gt;OctoNote&lt;/a&gt; and &lt;a href=&quot;https://apps.apple.com/us/app/fuzzzy-white-noise-for-sleep/id1336284372&quot;&gt;fuzZzy&lt;/a&gt; in a matter of days after years of inactivity. There&apos;s something incredible about sitting down with a clear product vision, a little bit of basic developer awareness, and access to Claude Opus. No roadblock seemed insurmountable.&lt;/p&gt;
&lt;p&gt;But you can&apos;t help but feel like there are thousands of other developers doing the exact same thing. Thousands more are taking it a step further with agent orchestration, vibe coding hundreds of thousands of lines of code on their way to the next big software scheme. It can feel overwhelming when you&apos;re not min-maxing your own time by coordinating the machines to further your own personal ends. Especially when it feels like &lt;a href=&quot;https://www.businessinsider.com/steve-yegge-vibecoding-author-predicts-layoffs-half-big-tech-engineers-2026-2&quot;&gt;hardship is on the horizon&lt;/a&gt; for professional software engineers.&lt;/p&gt;
&lt;p&gt;All of this brings me to my question. Which is essentially this:
&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;em&gt;What are we, as software craftspeople, doing about this existential threat?&lt;/em&gt;&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Good people should be making software&lt;/h2&gt;
&lt;p&gt;Simply put, if software is exponentially more build-able by more people than ever before, then more of us should be collectively building the products and platforms that we want to use and build for.&lt;/p&gt;
&lt;p&gt;I think this goes for everything. Social media, email &amp;amp; documents, file sharing, chat &amp;amp; collaboration, operating systems &amp;amp; app stores. If we have this window where AI use is relatively cheap (to the end user) and can accelerate smaller (better, more &quot;good&quot;) dev teams to build exponentially bigger products much faster, then that&apos;s what I would love to see happen.&lt;/p&gt;
&lt;p&gt;I guess I just wish that I was hearing stories about new software startups using AI to rebuild Discord or Gmail or whatever with an ethos that supported privacy, sustainability, and openness. Less stories about the existing powers that be weilding the threat of AI to hurt us.&lt;/p&gt;
&lt;h2&gt;We should be building collectively, and holding the line&lt;/h2&gt;
&lt;p&gt;I don&apos;t know if the answer is &apos;open source&apos; in a true sense - maybe it is - but I just feel like we should building this new software in the open in a way that it can&apos;t be abused in the name of pure profit for the powers that be.&lt;/p&gt;
&lt;p&gt;I think we should find a way to share our AI-accelerated code with each other - the software craftspeople &amp;amp; workers - so that we can compete and build new things that can compete with the giants.&lt;/p&gt;
&lt;p&gt;And we should celebrate people who value that mission, and hold strong.&lt;/p&gt;
&lt;h2&gt;Policy&lt;/h2&gt;
&lt;p&gt;Ultimately, we need to regulate. It&apos;s very disturbing to me how the rise of Gen AI is happening in an era where &lt;a href=&quot;https://www.theverge.com/ai-artificial-intelligence/824608/trump-executive-order-ai-state-laws&quot;&gt;our leaders are actively ignoring the threats&lt;/a&gt; posed by these billionaire-backed would-be monopolies. It&apos;s very dystopian, very cyberpunk in the most drab and dangerous way possible.&lt;/p&gt;
&lt;p&gt;We need leaders who can identify how to support the average person as AI has immediate impacts, and keep an eye on the horizon for what this technology is going to be and what our place in the new future is.&lt;/p&gt;
&lt;h1&gt;Summatim&lt;/h1&gt;
&lt;p&gt;I&apos;m no expert, and mostly just had to get this off my chest. Still processing a lot, and surely will for a long time moving forward. If you read this and felt anything, let me know &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;on Mastodon&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Other Links / References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;https://www.latent.space/p/ainews-why-openai-should-build-slack&lt;/li&gt;
&lt;li&gt;https://johan.hal.se/wrote/2026/02/03/the-sideprocalypse/&lt;/li&gt;
&lt;li&gt;https://taggart-tech.com/discord-alternatives/&lt;/li&gt;
&lt;li&gt;https://www.pcgamer.com/software/platforms/oh-good-discords-age-verification-rollout-has-ties-to-palantir-co-founder-and-panopticon-architect-peter-thiel/&lt;/li&gt;
&lt;li&gt;https://www.theverge.com/policy/830877/app-store-age-verification-act-pinterest-endorsement&lt;/li&gt;
&lt;li&gt;https://steipete.me/posts/2026/openclaw&lt;/li&gt;
&lt;li&gt;https://appfigures.com/resources/insights/20251205?f=2&lt;/li&gt;
&lt;li&gt;https://www.jeffgeerling.com/blog/2026/ai-is-destroying-open-source/&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>OctoNote 2.0</title><link>https://ajkueterman.com/posts/octonote-2</link><guid isPermaLink="true">https://ajkueterman.com/posts/octonote-2</guid><description>A new era for OctoNote! The GitHub-powered note-keeper app lives on.</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;style&amp;gt;
.app-store-badge-container {
display: flex;
justify-content: center;
margin: 2rem 0;
}&lt;/p&gt;
&lt;p&gt;.app-store-badge {
height: 60px;
width: auto;
display: block;
}&lt;/p&gt;
&lt;p&gt;.dark-mode-badge {
display: none;
}&lt;/p&gt;
&lt;p&gt;.dark .dark-mode-badge {
display: block;
}&lt;/p&gt;
&lt;p&gt;.dark .light-mode-badge {
display: none;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;In 2018 I got to go to WWDC, which was an incredible experience. I had so much fun. I still vividly remember sitting in the convention hall at one of the long wooden tables with my headphones on, jamming to the WWDC playlist, and banging out a new iOS app after having attended a few sessions on the new OAuth libraries &amp;amp; text API&apos;s.&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;wwdc-gallery&quot;&amp;gt;
&amp;lt;img src=&quot;/img/wwdc18/IMG_5789.jpeg&quot; alt=&quot;WWDC 2018&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/wwdc18/IMG_5815.jpeg&quot; alt=&quot;WWDC 2018&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/wwdc18/IMG_5824.jpeg&quot; alt=&quot;WWDC 2018&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/wwdc18/IMG_5854.jpeg&quot; alt=&quot;WWDC 2018&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.wwdc-gallery {
display: flex;
align-items: flex-end;
gap: 1rem;
overflow-x: auto;
margin: 2rem 0;
padding: 0.5rem 0;
scrollbar-width: thin;
}&lt;/p&gt;
&lt;p&gt;.wwdc-gallery img {
display: block;
height: 300px;
width: auto;
margin: 0;
padding: 0;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
align-self: flex-end;
transition: transform 0.2s ease;
}&lt;/p&gt;
&lt;p&gt;.wwdc-gallery img:first-child {
margin: 0;
}&lt;/p&gt;
&lt;p&gt;.wwdc-gallery::-webkit-scrollbar {
height: 8px;
}&lt;/p&gt;
&lt;p&gt;.wwdc-gallery::-webkit-scrollbar-track {
background: transparent;
}&lt;/p&gt;
&lt;p&gt;.wwdc-gallery::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}&lt;/p&gt;
&lt;p&gt;.dark .wwdc-gallery::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;I had been annoyed all week at flipping back and forth between Apple Notes on my phone and GitHub gists on my laptop to take notes during sessions. At the time I had been bouncing around between all sorts of note-taking iOS apps that had markdown support, but there wasn&apos;t a perfect solution out there. One that offered a great iOS &lt;em&gt;and&lt;/em&gt; desktop experience for editing markdown notes. There were apps that did it, but none that I enjoyed using.&lt;/p&gt;
&lt;p&gt;Basically I just wanted to be able to take notes on GitHub in the browser as GitHub gists and access them on my phone, which didn&apos;t exist. Even now, the GitHub first party app doesn&apos;t have great gist support - and they defintely don&apos;t focus on using gists as a way to edit &amp;amp; share markdown.&lt;/p&gt;
&lt;p&gt;So that&apos;s what I was working on that week. A way to view &amp;amp; edit my GitHub gists on my phone. That app eventually became &lt;a href=&quot;https://apps.apple.com/us/app/octonote/id1433164731&quot;&gt;&lt;strong&gt;OctoNote&lt;/strong&gt;&lt;/a&gt;, which shipped later that year.&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;octonote-gallery&quot;&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb.webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (1).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (2).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (3).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.octonote-gallery {
display: flex;
align-items: flex-end;
gap: 1rem;
overflow-x: auto;
margin: 2rem 0;
padding: 0.5rem 0;
scrollbar-width: thin;
}&lt;/p&gt;
&lt;p&gt;.octonote-gallery img {
display: block;
height: 400px;
width: auto;
margin: 0;
padding: 0;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
align-self: flex-end;
transition: transform 0.2s ease;
}&lt;/p&gt;
&lt;p&gt;.octonote-gallery img:first-child {
margin: 0;
}&lt;/p&gt;
&lt;p&gt;.octonote-gallery::-webkit-scrollbar {
height: 8px;
}&lt;/p&gt;
&lt;p&gt;.octonote-gallery::-webkit-scrollbar-track {
background: transparent;
}&lt;/p&gt;
&lt;p&gt;.octonote-gallery::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}&lt;/p&gt;
&lt;p&gt;.dark .octonote-gallery::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;Over the course of the next year or two, I shipped some enhancements to OctoNote, but otherwise it&apos;s been largely dormant. Over the last couple of OS releases, stability has gotten progressively bad, including issues with auth at startup that would block users from using the app normally. Time to either shelve it or update it!&lt;/p&gt;
&lt;p&gt;It wasn&apos;t the first time I&apos;d given myself that ultimatum and inevitably kicked the can down the road. But this year, with the power of Claude Opus to help me plan and execute my app update vision, I was able to carve out just enough of my limited personal time to dedicate myself to updating the app. Claude enabled me to quickly hurdle previous stumbling blocks I had with my limited/aging iOS experience. It saved me from a ton of boilerplate writing and helped me focus on the product vision. Maybe most critically, it helped me reason through the arcane rules of the App Store and some of Apple&apos;s APIs that still lack good documentation for newcomers.&lt;/p&gt;
&lt;p&gt;The result is a very familiar OctoNote, but one that works and is updated for the latest versions of iOS &amp;amp; iPadOS (+ macOS via Catalyst). Old IAPs and subscriptions work as they did before, similar upgrade features exist for general parity with version 1.1, and, in general, the app is just the same but more stable. I&apos;m honestly proud of how similar it is to my original vision, and how it still holds a valuable place in my workflow.&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;octonote-gallery&quot;&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (4).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (5).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (6).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;img src=&quot;/img/octonote/460x996bb (7).webp&quot; alt=&quot;OctoNote Screenshot&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;In the future, I hope to continue iterating on the value prop of OctoNote while bringing people some cool new features. I hope you&apos;ll follow along!&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;app-store-badge-container&quot;&amp;gt;
&amp;lt;a href=&quot;https://apps.apple.com/us/app/octonote/id1433164731&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;
&amp;lt;img
src=&quot;/img/octonote/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917.svg&quot;
alt=&quot;Download on the App Store&quot;
class=&quot;app-store-badge light-mode-badge&quot;
/&amp;gt;
&amp;lt;img
src=&quot;/img/octonote/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg&quot;
alt=&quot;Download on the App Store&quot;
class=&quot;app-store-badge dark-mode-badge&quot;
/&amp;gt;
&amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Getting games from your Mac to your PS2</title><link>https://ajkueterman.com/posts/getting-games-on-your-ps2-from-mac</link><guid isPermaLink="true">https://ajkueterman.com/posts/getting-games-on-your-ps2-from-mac</guid><description>I recently worked through some basic issues with PS2 modding on macOS, and wanted to leave my notes.</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There aren&apos;t great guides out there for PS2 modding using a Mac. This guide just covers the steps I went through to format my SSD and get games on it from my Mac. Nothing fancy here.&lt;/p&gt;
&lt;h2&gt;What I used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Free McBoot (FCMB) Memory card&lt;/li&gt;
&lt;li&gt;1TB SATA SSD&lt;/li&gt;
&lt;li&gt;SATA to USB adapter&lt;/li&gt;
&lt;li&gt;SATA to PS2 adapter&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Format the Drive Using Your PS2 Using uLaunchELF&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Install your SSD into the network adapter and connect it to your PS2&lt;/li&gt;
&lt;li&gt;Boot your PS2 with the FMCB memory card&lt;/li&gt;
&lt;li&gt;Launch &lt;strong&gt;uLaunchELF&lt;/strong&gt; (also called wLaunchELF) from the FMCB menu&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;FileBrowser → MISC → HddManager&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The HDD Manager should detect your new drive and show it as unformatted&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;R1&lt;/strong&gt; to open the menu&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Format&lt;/strong&gt; (choose &lt;strong&gt;48bit HDLoader&lt;/strong&gt; if you get an option)&lt;/li&gt;
&lt;li&gt;Wait for formatting to complete&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After formatting, your drive has the correct APA partition scheme and is ready for games.&lt;/p&gt;
&lt;h2&gt;Part 2: Compile hdl-dump on Your Mac&lt;/h2&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;You&apos;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xcode Command Line Tools&lt;/strong&gt; (if not installed: &lt;code&gt;xcode-select --install&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homebrew&lt;/strong&gt; (if not installed: visit https://brew.sh)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GNU Make&lt;/strong&gt; (install via: &lt;code&gt;brew install make&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git&lt;/strong&gt; (pre-installed on Mac / with Xcode tools)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Clone and Compile hdl-dump&lt;/h3&gt;
&lt;p&gt;Open Terminal and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Clone the repository
git clone https://github.com/ps2homebrew/hdl-dump.git
cd hdl-dump

# Build with `make`
make RELEASE=yes IIN_OPTICAL_MMAP=no
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After compilation, you&apos;ll have &lt;code&gt;hdl_dump&lt;/code&gt; executable in the folder.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Connect the SSD via USB&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Connect your SSD to your Mac via your USB-to-SATA cable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Important:&lt;/strong&gt; macOS won&apos;t mount the drive (it&apos;s APA format) — that&apos;s expected, ignore warnings&lt;/li&gt;
&lt;li&gt;Find the drive&apos;s device path:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;diskutil list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for your 1TB SSD. It will be something like &lt;code&gt;/dev/disk4&lt;/code&gt; (the number varies). You should make sure it&apos;s the exact right size as your SSD and clearly isn&apos;t an important / other drive. &lt;strong&gt;Be absolutely certain you identify the correct disk!&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Install Games&lt;/h2&gt;
&lt;h3&gt;List installed games on the HDD:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo ./hdl_dump toc /dev/diskX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Replace X with your disk number)&lt;/p&gt;
&lt;h3&gt;Install a game:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo ./hdl_dump inject_dvd /dev/diskX &quot;Game Name&quot; /path/to/game.iso
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For CD-based games (some PS2 games are on CD):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ./hdl_dump inject_cd /dev/diskX &quot;Game Name&quot; /path/to/game.iso
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Verifying Installation&lt;/h2&gt;
&lt;p&gt;After installing games, verify they&apos;re on the drive:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ./hdl_dump toc /dev/diskX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see your games listed with their names and sizes.&lt;/p&gt;
</content:encoded></item><item><title>Sorry to my PR reviewers</title><link>https://ajkueterman.com/posts/sorry-pr-reviewers</link><guid isPermaLink="true">https://ajkueterman.com/posts/sorry-pr-reviewers</guid><description>A brief explanation of my PR drafting foibles.</description><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I just want to take a moment to apologize to my awesome colleagues who review my pull requests with amazing diligence, care, and kindness. I just need to say I appreciate you and your patience, for dealing with my particular brand of branch-pushing and PR-staging.&lt;/p&gt;
&lt;h2&gt;I’m a Lion&lt;/h2&gt;
&lt;p&gt;My preamble - I’m a &lt;a href=&quot;https://www.devonduvets.com/news/post/sleep-chronotypes-and-your-sleep&quot;&gt;Lion Chronotype&lt;/a&gt;, meaning that I start (early) with energy and lose steam over the course of the day. So my productivity looks pretty much like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/energy-over-time.png&quot; alt=&quot;energy over time graph showing a linear progression of energy going down from 6AM to 10PM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I wake up with energy, new ideas, and focus. At the end of the day, I’m usually a little bit slower, less sharp. It’s pretty much a directly linear graph, with maybe a small bump from mid-morning caffeine.&lt;/p&gt;
&lt;h2&gt;Abusing drafts&lt;/h2&gt;
&lt;p&gt;Unfortunately, usually my PRs come as I’m wrapping up work for the day. I just get the urge to stop, commit, and then summarize where I left off, and often times a PR is a good way to do that.&lt;/p&gt;
&lt;p&gt;These PRs usually come in ‘draft&apos; status, with a big &lt;code&gt;WIP&lt;/code&gt; label slapped on them, and usually some indication in the description that things are in-flight. This doesn’t stop the prying yet helpful eyes from making their way to this new implicit proposal for changes.&lt;/p&gt;
&lt;p&gt;The result — my PRs inevitably end up being of better quality.&lt;/p&gt;
&lt;h2&gt;Side effects&lt;/h2&gt;
&lt;p&gt;Boo hoo, right? The downside here is twofold.&lt;/p&gt;
&lt;h3&gt;Time “wasted”&lt;/h3&gt;
&lt;p&gt;I end up wasting my co-workers time, as they have to spend time reviewing a PR multiple times. They also inevitably spot issues or opportunities that require comments / feedback, some of which might not have been there by the time they had their next morning’s coffee.&lt;/p&gt;
&lt;h3&gt;Impostor mode activated&lt;/h3&gt;
&lt;p&gt;Pushing a PR is implicitly exposing yourself to critique and constructive (hopefully) criticism. Other devs have to scrutinize the code you’ve written and evaluate it. This is naturally a vulnerable place to be for any dev.&lt;/p&gt;
&lt;p&gt;By pushing branches and opening drafts before they’re fully ready, I expose myself to feedback on code that may (likely) not be my best work. Not always great for the confidence.&lt;/p&gt;
&lt;h2&gt;Getting better&lt;/h2&gt;
&lt;p&gt;I think my personal goal in writing this post was just to acknowledge a questionable dev habit. I think there are probably better ways to handle this, but I also think there is some merit to how I manage my PRs.&lt;/p&gt;
&lt;p&gt;For example, I could probably be a bit more thoughtful about consulting my own ‘definition of ready’ when opening a PR. If it’s after 5 and you’ve just finished running the build with your latest fix to the point where it ‘works’ — it is actually okay to take a step back and just make a quick commit and leave it at that. Let ‘morning AJ’ get another crack at it before the rest of the team.&lt;/p&gt;
&lt;p&gt;On the flip side, don’t be the dev that holds back PRs until the last possible second. Especially if your team relies heavily on them to communicate changes. Polishing code to a perfect sheen only to learn that you’ve missed something structural is the opposite danger here.&lt;/p&gt;
&lt;p&gt;Anyway, thanks again to the folks who review my PRs. The little extra effort does not go unnoticed, and I appreciate the grace 😇&lt;/p&gt;
</content:encoded></item><item><title>Hilt Navigation ViewModel is Perfect for Compose</title><link>https://ajkueterman.com/posts/hilt-view-model-is-awesome</link><guid isPermaLink="true">https://ajkueterman.com/posts/hilt-view-model-is-awesome</guid><description>A quick highlight of the Hilt Navigation for Compose library.</description><pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&apos;ve already got &lt;a href=&quot;https://dagger.dev/hilt/migration-guide.html&quot;&gt;Hilt up and running&lt;/a&gt;, and you&apos;re willing to utilize the &lt;a href=&quot;https://developer.android.com/develop/ui/compose/navigation&quot;&gt;Navigation library&lt;/a&gt; for your Compose UI, the &lt;a href=&quot;https://developer.android.com/develop/ui/compose/libraries#hilt-navigation&quot;&gt;Hilt Navigation Compose library&lt;/a&gt; is a no brainer.&lt;/p&gt;
&lt;p&gt;Essentially, what it allows you to do is get an Android Architecture &lt;code&gt;ViewModel&lt;/code&gt; scoped to a navigation &lt;a href=&quot;https://developer.android.com/guide/navigation/use-graph/navigate#example&quot;&gt;destination&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s my modified version of the example from &lt;a href=&quot;https://developer.android.com/develop/ui/compose/libraries#hilt-navigation&quot;&gt;the docs&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Composable
fun MyApp() {
    val navController = rememberNavController()
    var startRoute by remember { mutableStateOf(&quot;example&quot;) }
    NavHost(navController, startDestination = startRoute) {
        composable(&quot;example&quot;) { backStackEntry -&amp;gt;
            val viewModel: MyViewModel = hiltViewModel()
            MyScreen(viewModel)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example you have a &lt;code&gt;NavHost&lt;/code&gt; setup that you&apos;re using to navigate between screens. Inside your &lt;code&gt;example&lt;/code&gt; destination, you use the &lt;code&gt;hiltViewModel()&lt;/code&gt; function to get a &lt;code&gt;ViewModel&lt;/code&gt; scoped to the destination that can then be passed into the &lt;code&gt;MyScreen&lt;/code&gt; composable.&lt;/p&gt;
&lt;p&gt;You may be wondering why this matters, as you could always use the &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/lifecycle/viewmodel/compose/package-summary#viewmodel&quot;&gt;&lt;code&gt;viewModel()&lt;/code&gt;&lt;/a&gt; method to get a &lt;code&gt;ViewModel&lt;/code&gt; in Compose. The key distinction here is &lt;a href=&quot;https://developer.android.com/develop/ui/compose/libraries#viewmodel&quot;&gt;scoping&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;viewModel()&lt;/code&gt; returns an existing &lt;code&gt;ViewModel&lt;/code&gt; or creates a new one. By default, the returned &lt;code&gt;ViewModel&lt;/code&gt; is scoped to the enclosing activity, fragment or navigation destination, and is retained as long as the scope is alive.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you&apos;re utilizing Compose Navigation, it just makes sense to be able to scope View Models to the nav destination, and further separate from a direct connection to Fragments or Activities.&lt;/p&gt;
</content:encoded></item><item><title>Android view binding Easy Setup</title><link>https://ajkueterman.com/posts/view-binding-setup</link><guid isPermaLink="true">https://ajkueterman.com/posts/view-binding-setup</guid><description>Super quick startup guide for setting up view binding in Activities.</description><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Jetpack Compose is the way. But sometimes, you still need Fragments and XML. And if you have to deal with that, you&apos;ll probably want to use &lt;a href=&quot;https://developer.android.com/topic/libraries/view-binding&quot;&gt;view binding&lt;/a&gt;. Writing this quick reminder for my future self for when I find myself in that predicament.&lt;/p&gt;
&lt;p&gt;To enable &lt;a href=&quot;https://developer.android.com/topic/libraries/view-binding&quot;&gt;view binding&lt;/a&gt;, add the following to your module build.gradle file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;android {
    ...
    buildFeatures {
        viewBinding = true
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This automatically generates a binding class for each layout. In my example &lt;code&gt;activity_whatever.xml&lt;/code&gt; becomes &lt;code&gt;ActivityWhateverBinding&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then in your &lt;code&gt;Activity&lt;/code&gt; (&lt;em&gt;&lt;code&gt;AppCompatActivity&lt;/code&gt; in this case&lt;/em&gt;) declare a &lt;code&gt;lazy val&lt;/code&gt; for your &lt;code&gt;binding&lt;/code&gt; and inflate it. Then &lt;code&gt;setContentView&lt;/code&gt; to &lt;code&gt;binding.root&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class WhateverActivity: AppCompatActivity() {
    private val binding: ActivityWhateverBinding by lazy { ActivityWhateverBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a super quick &amp;amp; easy way to bind your XML. So simple that you&apos;ll avoid the urge to setup some base class just for making binding easier. If you were using a different Activity type, this also makes it easier to move to &lt;code&gt;AppCompatActivity&lt;/code&gt; or &lt;code&gt;ComponentActivity&lt;/code&gt; in the future to enable Hilt.&lt;/p&gt;
</content:encoded></item><item><title>Create Your Own LOGAF Scale for Pull Request Reviews</title><link>https://ajkueterman.com/posts/making-your-own-logaf-scale-for-pull-requests</link><guid isPermaLink="true">https://ajkueterman.com/posts/making-your-own-logaf-scale-for-pull-requests</guid><description>Easily indicate the priority of your PR review comments with the &apos;LOGAF&apos; scale.</description><pubDate>Tue, 31 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I like the idea of the &lt;a href=&quot;https://blog.danlew.net/2020/04/15/the-logaf-scale/&quot;&gt;LOGAF scale&lt;/a&gt;, where we can easily indicate if suggestions to PR review or other feedback is given in &apos;&lt;em&gt;levels of give-a-f*ck&lt;/em&gt;&apos;. A scale like this helps collaborators prioritize and contextualize your feedback quickly in a way that&apos;s pretty straightforward.&lt;/p&gt;
&lt;p&gt;While I love the raw LOGAF model, I do sometimes hesitate to share this philosophy with everyone at my company based on varying levels of comfort with profanity. Instead, I took the time just to indicate my own low/medium/high &apos;level of caring&apos; that I can share on PR reviews. I also took the time to briefly spell out what I expect from each level. Now in PRs I can include a &apos;LOGAF level&apos; link and move on.&lt;/p&gt;
&lt;p&gt;Below is my basic scale of 3 levels, the description of each, and the link I&apos;d include in each comment.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Low&lt;/h2&gt;
&lt;p&gt;A this level I usually just noticed something that had a slight code smell. I may spot things that are tangential to your change or just things I &apos;noticed&apos; when reviewing.&lt;/p&gt;
&lt;p&gt;You can self-resolve these comments in GitHub without a comment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[🟢 low](https://ajkueterman.com/posts/making-your-own-logaf-scale-for-pull-requests/#low)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Medium&lt;/h2&gt;
&lt;p&gt;At this level I feel changes should be made but can be convinced with a good reason, a follow up story/documentation/etc. that captures the changes in future work or clarification.&lt;/p&gt;
&lt;p&gt;You can self-resolve these comments in GitHub with a comment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[🟡 medium](https://ajkueterman.com/posts/making-your-own-logaf-scale-for-pull-requests/#medium)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;High&lt;/h2&gt;
&lt;p&gt;At this level I feel like changes need to be made before this PR is merged. In exceptional cases, follow up stories &amp;amp; documentation may resolve these concerns, but they would usually require a conversation with me or some other SME.&lt;/p&gt;
&lt;p&gt;Do not self-resolve these comments in GitHub. Make changes / leave comments and allow me to resolve them before merging.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[🔴 high](https://ajkueterman.com/posts/making-your-own-logaf-scale-for-pull-requests/#high)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;If you decide to implement your own LOGAF scale for your PR reviews let me know on Mastodon &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;@aj on androiddev.social&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Other PR Review documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://deepsource.com/blog/code-review-best-practices&quot;&gt;Code review best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>How to Use TalkBack on an Android Emulator</title><link>https://ajkueterman.com/posts/how-to-use-talkback-on-an-android-emulator</link><guid isPermaLink="true">https://ajkueterman.com/posts/how-to-use-talkback-on-an-android-emulator</guid><description>How to use the Android Accessibility Suite to enable the TalkBack screen reader on your Android Emulator.</description><pubDate>Wed, 26 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A huge part of making our apps accessible is enabling access for those who have limited vision. This can range from allowing users to adjust the size of the UI or the text in our apps - to making sure that accessibility tools like screen readers can parse the UI of our apps.&lt;/p&gt;
&lt;p&gt;The screen reading tool provided by Google on Android devices is called TalkBack. This app is usually installed by default on Android devices, letting users operate their phones without needing to see the screen. For developers, we can use TalkBack to &lt;a href=&quot;https://developer.android.com/guide/topics/ui/accessibility&quot;&gt;ensure our app is accessible&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, you may have noticed that this behavior isn&apos;t available by default when you create a new Android Emulator in Android Studio - a problem for developers &amp;amp; testers that don&apos;t have access to physical devices.&lt;/p&gt;
&lt;p&gt;Let&apos;s walk through the process of getting TalkBack set up on your Android Emulators.&lt;/p&gt;
&lt;h2&gt;Create A New Emulator&lt;/h2&gt;
&lt;p&gt;In Android Studio (I&apos;m using Android Studio Flamingo 2022.2.1), open the Device Manager and choose Create Device.&lt;/p&gt;
&lt;h3&gt;Select Hardware&lt;/h3&gt;
&lt;p&gt;Select a device you&apos;d like to create an Emulator of, &lt;strong&gt;making sure to select one that has Play Store access&lt;/strong&gt;. You can tell by the Play Store icon visible in the &apos;Play Store&apos; column of the selection list.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/talkback/select-play-store-device.png&quot; alt=&quot;selecting play store enabled device screen shot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After selecting the appropriate hardware, click Next.&lt;/p&gt;
&lt;h3&gt;System Image&lt;/h3&gt;
&lt;p&gt;Select or download an Android OS image. I chose to download &amp;amp; select Android Tiramisu (API 33), though any modern OS will do.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/talkback/system-image.png&quot; alt=&quot;selecting system image screen shot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once that is downloaded &amp;amp; selected, click Next.&lt;/p&gt;
&lt;h3&gt;AVD Setup&lt;/h3&gt;
&lt;p&gt;We&apos;ve done all the hard work, no changes needed here unless you need to make specific adjustments to your Emulator setup. Click Finish.&lt;/p&gt;
&lt;h2&gt;Download the Android Accessibility Suite&lt;/h2&gt;
&lt;p&gt;Back in Device Manager, click the &apos;play&apos; button on your newly added device to run it.&lt;/p&gt;
&lt;p&gt;If, at this point, we decided to go into Settings and look for TalkBack, we won&apos;t get any results. This is the default behavior for all new emulators you create. Let&apos;s fix that by downloading the &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback&amp;amp;hl=en_US&amp;amp;gl=US&quot;&gt;Android Accessibility Suite&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Sign In &amp;amp; Search&lt;/h3&gt;
&lt;p&gt;Your device should have the Play Store app already installed. Open it, and sign in with your Google account. You may have to go through a few setup steps when logging in to the Play Store, as this emulator is treated as a &apos;new&apos; device.&lt;/p&gt;
&lt;p&gt;In the Play Store search for &apos;android accessibility suite&apos; and download it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/talkback/aas-search.png&quot; alt=&quot;searching for android accessibility suite&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Using TalkBack&lt;/h3&gt;
&lt;p&gt;After it downloads and installs, close the Play Store and look at your list of installed apps. You may notice that the Accessibility Suite doesn&apos;t show up here. That&apos;s expected for this very specific app.&lt;/p&gt;
&lt;p&gt;Instead, open Settings, and search for TalkBack. Here you&apos;ll see the TalkBack option, which brings you to the Screen Reader section of the device&apos;s Accessibility settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/talkback/talkback-settings.png&quot; alt=&quot;accessibility settings on device with TalkBack&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ta-da! 🥳 Now we can use TalkBack just like we would on a normal Android device.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/talkback/talkback-page.png&quot; alt=&quot;talkback landing page&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;TalkBack Resources&lt;/h3&gt;
&lt;p&gt;For more on testing your apps for accessibility with TalkBack, check out these great resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=_1yRVwhEv5I&quot;&gt;TalkBack - Accessibility on Android (video)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://accessibility.huit.harvard.edu/test-android-talkback&quot;&gt;Testing with TalkBack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/guide/topics/ui/accessibility/testing&quot;&gt;Google&apos;s Guide to Testing Accessibility&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go forth and build accessible apps!&lt;/p&gt;
</content:encoded></item><item><title>Visualizing Data with Swift Charts</title><link>https://ajkueterman.com/posts/visualizing-data-with-swift-charts</link><guid isPermaLink="true">https://ajkueterman.com/posts/visualizing-data-with-swift-charts</guid><description>Software UI development is often about visualizing the data available on our devices. A simple chart can communicate so much at a glance.</description><pubDate>Tue, 11 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is a re-post of the article I shared &lt;a href=&quot;https://base11studios.com/ios/swift/swiftui/charts/2023/04/06/pretty-swiftui-line-charts/&quot;&gt;@ the Base11 Studios blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/swift-charts.png&quot; alt=&quot;3 charts displaying random data&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, with &lt;a href=&quot;https://developer.apple.com/documentation/charts&quot;&gt;Swift Charts&lt;/a&gt; we can easily make that a reality in our apps. Take, for example, a basic Line Chart like the one used in &lt;a href=&quot;https://base11studios.com/clients/calicalo/&quot;&gt;CaliCalo&lt;/a&gt; to display a user&apos;s Diet vs. Active calorie count over seven days.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/calicalo-trends-screen-closeup.png&quot; alt=&quot;Trends screen of CaliCalo depicting a line graph displayed on iPhone&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With this screen, we want to communicate essential but pivotal data quickly. How have you been balancing your NET calories (calories consumed vs. calories burned) for the last seven days? We plot two lines - one for calories consumed (diet) and one for calories burned by daily totals.&lt;/p&gt;
&lt;p&gt;By quickly scanning the graph, users can see how their lines overlap. Depending on their goals, this can help inform progress.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For a user trying to maintain weight, the goal may be to have the lines match up as closely as possible.&lt;/li&gt;
&lt;li&gt;A user trying to lose weight would be trying to consistently keep the &apos;Diet&apos; line below the &apos;Burned&apos; line. Opposite for the user trying to gain weight.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For either user, the line graph can help identify significant disparities or inconsistencies that can help guide behavior or tracking.&lt;/p&gt;
&lt;h2&gt;Swift Charts&lt;/h2&gt;
&lt;p&gt;Apple&apos;s &lt;a href=&quot;https://developer.apple.com/documentation/charts&quot;&gt;Swift Charts&lt;/a&gt; library offers a quick and easy way to &lt;a href=&quot;https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts&quot;&gt;start plotting data&lt;/a&gt;. We&apos;ll use this library, along with SwiftUI, to build out our Trends chart.&lt;/p&gt;
&lt;h3&gt;Brief Overview&lt;/h3&gt;
&lt;p&gt;A quick explainer of Swift Charts in the world of SwiftUI. You have a &lt;code&gt;Chart&lt;/code&gt; view that takes a body that should be a list of &apos;marks.&apos; Marks can be things like &lt;a href=&quot;https://developer.apple.com/documentation/charts/barmark&quot;&gt;&lt;code&gt;BarMark&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://developer.apple.com/documentation/charts/linemark&quot;&gt;&lt;code&gt;LineMark&lt;/code&gt;&lt;/a&gt;, or &lt;a href=&quot;https://developer.apple.com/documentation/charts/pointmark&quot;&gt;&lt;code&gt;PointMark&lt;/code&gt;&lt;/a&gt; that plot out the data depending on the type of chart you&apos;re making.&lt;/p&gt;
&lt;p&gt;Add to this &lt;code&gt;Chart&lt;/code&gt; (and its marks) some modifier functions that allow us to declare how our data dimensions are displayed &amp;amp; styled, and we have a pretty chart showing data at a glance.&lt;/p&gt;
&lt;h3&gt;Make a Mark&lt;/h3&gt;
&lt;p&gt;Let&apos;s look at a straightforward example. Here, we&apos;re charting calories per day using some brute force. Let&apos;s create a &lt;code&gt;Chart&lt;/code&gt; with some &lt;code&gt;LineMark&lt;/code&gt;s.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SwiftUI
import Charts

struct SimpleLineChartView: View {
    var body: some View {
        VStack {
            Chart {
                LineMark(
                    x: .value(&quot;Day&quot;, Calendar.current.date(from: .init(year: 2023, month: 1, day: 1)) ?? Date()),
                    y: .value(&quot;Calories&quot;, 2000.0)
                )
                LineMark(
                    x: .value(&quot;Day&quot;, Calendar.current.date(from: .init(year: 2023, month: 1, day: 2)) ?? Date()),
                    y: .value(&quot;Calories&quot;, 1800.0)
                )
                LineMark(
                    x: .value(&quot;Day&quot;, Calendar.current.date(from: .init(year: 2023, month: 1, day: 3)) ?? Date()),
                    y: .value(&quot;Calories&quot;, 2300.0)
                )
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code above results in a simple, practical-looking chart that plots our 3 points on a line.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/line-chart-1a.png&quot; alt=&quot;basic line chart with 3 points&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Cool to see some data visualized! Let&apos;s look at a slightly more complicated example, where we map two lines. This time, we&apos;re plotting the two types of calories we care about in &lt;a href=&quot;https://base11studios.com/clients/calicalo/&quot;&gt;CaliCalo&lt;/a&gt; - Diet and Burned. To do this, we need to plot 2 points for every day.&lt;/p&gt;
&lt;p&gt;If we were to add a bunch of points (manually like in our example above or by looping over some data), we&apos;d end up with a chart that looks something like what you see below—one long continuous line on the chart zigging back and forth.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/line-chart-2a.png&quot; alt=&quot;basic line chart with 14 points&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Technically, all the data has been charted, but visually we aren&apos;t differentiating between the type of data supplied to the &lt;code&gt;Chart&lt;/code&gt;. I think we should do that next.&lt;/p&gt;
&lt;h3&gt;Add a Modifier&lt;/h3&gt;
&lt;p&gt;Swift Charts uses modifier functions to add dimensions to how the data is displayed. It can help separate data sets and display them using different colors/shapes/curves etc. The first one we&apos;ll utilize is &lt;a href=&quot;https://developer.apple.com/documentation/charts/chartcontent/foregroundstyle(by:)&quot;&gt;&lt;code&gt;foregroundStyle(by:)&lt;/code&gt;&lt;/a&gt;, which we can use to change how we display our data set.&lt;/p&gt;
&lt;p&gt;Let&apos;s do a little prep first to make our &lt;code&gt;Chart&lt;/code&gt; function a bit easier to read (and make it a more realistic example).&lt;/p&gt;
&lt;p&gt;First, let&apos;s create some &lt;code&gt;struct&lt;/code&gt;s to represent data points for our calorie data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Diet {
    let dateLabel: String = &quot;Day&quot;
    let date: Date
    let valueLabel: String = &quot;Diet&quot;
    let value: Double
}

struct Burned {
    let dateLabel: String = &quot;Day&quot;
    let date: Date
    let valueLabel: String = &quot;Burned&quot;
    let value: Double
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, let&apos;s create some test data. We&apos;ll create a list of &lt;code&gt;Diet&lt;/code&gt; objects we can use to populate the chart for a 5-day period.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let diet: [Diet] = [
    Diet(
        date: Calendar.current.date(
            from: .init(
                year: 2023,
                month: 1,
                day: 1
            )
        ) ?? Date(),
        value: 2000.0
    ),
    Diet(
        date: Calendar.current.date(
            from: .init(
                year: 2023,
                month: 1,
                day: 2
            )
        ) ?? Date(),
        value: 1800.0
    ),
    Diet(
        date: Calendar.current.date(
            from: .init(
                year: 2023,
                month: 1,
                day: 3
            )
        ) ?? Date(),
        value: 2300.0
    ),
    Diet(
        date: Calendar.current.date(
            from: .init(
                year: 2023,
                month: 1,
                day: 4
            )
        ) ?? Date(),
        value: 2100.0
    ),
    Diet(
        date: Calendar.current.date(
            from: .init(
                year: 2023,
                month: 1,
                day: 5
            )
        ) ?? Date(),
        value: 1500.0
    ),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;In the following snippet, we&apos;ll use this same list twice to map two lines, but in a real example, we would have two separate lists of test data that we would chart in the next step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Using this list of data, we can now chart two lines. We&apos;ll use the &lt;a href=&quot;https://developer.apple.com/documentation/charts/chartcontent/foregroundstyle(by:)&quot;&gt;&lt;code&gt;foregroundStyle(by:)&lt;/code&gt;&lt;/a&gt; function to define each &lt;code&gt;LineMark&lt;/code&gt;&apos;s style for each item in the list. It&apos;s a way of indicating how to display a given mark.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct SimpleLineChartView: View {
    var body: some View {
        VStack {
            Chart {
                ForEach(diet, id: \.date){ dataPoint in
                    // &apos;Burned&apos; Calorie data, from our list of `Diet` objects
                    LineMark(x: .value(dataPoint.dateLabel, dataPoint.date), y: .value(dataPoint.valueLabel, dataPoint.value))
                        .foregroundStyle(by: .value(&quot;BURNED&quot;, &quot;BURNED&quot;))
                    // &apos;Diet&apos; Calorie data, also from our list of `Diet` objects, but shifted by -300 calories
                    LineMark(x: .value(dataPoint.dateLabel, dataPoint.date), y: .value(dataPoint.valueLabel, (dataPoint.value - 300.0)))
                        .foregroundStyle(by: .value(&quot;DIET&quot;, &quot;DIET&quot;))
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;ll get a chart that looks like this without any extra setup.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/line-chart-3a.png&quot; alt=&quot;basic line chart with 2 lines&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A line chart with two lines of five points each (using our &lt;code&gt;diet&lt;/code&gt; array x2). We even have a cute little legend at the bottom left indicating what the lines mean by color.&lt;/p&gt;
&lt;h4&gt;Add some &lt;em&gt;Style&lt;/em&gt;&lt;/h4&gt;
&lt;p&gt;Green and blue are just the API&apos;s default colors without us doing any extra work. If we want to add some of our special branding sauce, we use another simple modifier. On the &lt;code&gt;Chart&lt;/code&gt;, we can apply the &lt;a href=&quot;https://developer.apple.com/documentation/charts/chartplotcontent/chartforegroundstylescale(_:)&quot;&gt;&lt;code&gt;.chartForegroundStyleScale(_:)&lt;/code&gt;&lt;/a&gt; modifier to define the colors we want to use for each of our data sets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Chart {
    /// ...
}
.chartForegroundStyleScale([&quot;DIET&quot;: Color.orange, &quot;BURNED&quot;: Color.blue])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With only that 1 line, we&apos;ve got some nice &lt;a href=&quot;https://base11studios.com/clients/calicalo/&quot;&gt;CaliCalo&lt;/a&gt; colors:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/swift-charts/line-chart-4a.png&quot; alt=&quot;basic line chart with 2 lines and CaliCalo colors&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Modifiers by Example&lt;/h3&gt;
&lt;p&gt;Now that we understand some of the basic building blocks of Swift Charts let&apos;s look at a few more ways to modify our chart style. Ultimately, we should have a chart that looks close to what we&apos;ve built for &lt;a href=&quot;https://base11studios.com/clients/calicalo/&quot;&gt;CaliCalo&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Chart&lt;/strong&gt; Modifiers&lt;/h4&gt;
&lt;p&gt;These modifiers are applied to the &lt;code&gt;Chart&lt;/code&gt; view.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modifier&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chart/chartxscale(range:type:)&quot;&gt;&lt;code&gt;.chartXScale(range:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Add 20 pts. of padding to the X axis.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-5a.png&quot; alt=&quot;line chart with x scale padding&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chart/chartxaxis(content:)&quot;&gt;&lt;code&gt;.chartXAxis(content:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;AxisMarks&lt;/code&gt; to remove Y axis lines.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-5b.png&quot; alt=&quot;line chart with x axis simplifcation&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chart/chartplotstyle(content:)&quot;&gt;&lt;code&gt;.chartPlotStyle(content:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Set the height of the chart contents within the Chart view.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-5c.png&quot; alt=&quot;line chart with plot style height&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chart/chartyaxis(content:)&quot;&gt;&lt;code&gt;.chartYAxis(content:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Move Y Axis labels to leading edge.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-5d.png&quot; alt=&quot;line chart with plot style height&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;&lt;strong&gt;LineMarks&lt;/strong&gt; Modifiers&lt;/h4&gt;
&lt;p&gt;We can apply some additional modifiers to the &lt;code&gt;LineMark&lt;/code&gt;s to improve the overall look and feel of our Chart lines.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modifier&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chartcontent/interpolationmethod(_:)&quot;&gt;&lt;code&gt;.interpolationMethod(_:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;.catmullRom&lt;/code&gt; as the interpolation method.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-6a.png&quot; alt=&quot;line chart with catmull rom&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.apple.com/documentation/charts/chartcontent/symbol(_:)&quot;&gt;&lt;code&gt;.symbol(_:)&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;.square&lt;/code&gt; and &lt;code&gt;.circle&lt;/code&gt; as the line mark symbols.&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-6b.png&quot; alt=&quot;line chart with square and circle marks&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Modifiers Combined&lt;/h4&gt;
&lt;p&gt;Here&apos;s how those modifiers are provided to the Chart and Marks to make the last chart example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
Chart {
    ForEach(diet, id: \.date){ dataPoint in
        LineMark(x: .value(dataPoint.dateLabel, dataPoint.date), y: .value(dataPoint.valueLabel, dataPoint.value))
            .foregroundStyle(by: .value(&quot;BURNED&quot;, &quot;BURNED&quot;))
            .interpolationMethod(.catmullRom)
            .symbol(.square)
        LineMark(x: .value(dataPoint.dateLabel, dataPoint.date), y: .value(dataPoint.valueLabel, (dataPoint.value - 300.0)))
            .foregroundStyle(by: .value(&quot;DIET&quot;, &quot;DIET&quot;))
            .interpolationMethod(.catmullRom)
            .symbol(.circle)
    }
}
.chartForegroundStyleScale([&quot;DIET&quot;: Color.orange, &quot;BURNED&quot;: Color.blue])
.chartXScale(range: .plotDimension(padding: 20.0))
.chartXAxis{
    AxisMarks(preset: .aligned, position: .top, values: .stride(by: .day)){ value in
        AxisValueLabel(format: .dateTime.day().weekday(.narrow))
    }
}
.chartPlotStyle{plotArea in
    plotArea.frame(maxWidth: .infinity, minHeight: 250.0, maxHeight: 250.0)
}
.chartYAxis{
    AxisMarks(position: .leading)
}
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Wrapping Up 🎁&lt;/h3&gt;
&lt;p&gt;Combined, we can design a chart that closely mirrors our &lt;a href=&quot;https://base11studios.com/clients/calicalo/&quot;&gt;CaliCalo&lt;/a&gt; Trends UI.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Our Example&lt;/th&gt;
&lt;th&gt;Trends UI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-6b.png&quot; alt=&quot;line chart with square and circle marks&quot; /&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/img/swift-charts/line-chart-7a.png&quot; alt=&quot;trends line chart&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We also learned about the basic building blocks of Swift Charts and how easily modifiers can be applied to make Charts our own.&lt;/p&gt;
&lt;p&gt;For the complete source of this example, plus a more complex example that includes some randomized test-data-generation, check out our &lt;a href=&quot;https://github.com/Base11Studios/trends-chart-demo&quot;&gt;Trends Chart Repo on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;hr&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🐘  If you liked this article and want more tech content (and other nerd commentary) you can follow me on Mastodon. I hang out at AndroidDev.social &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;@aj&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Make &apos;Dragon Heist&apos; Your Own</title><link>https://ajkueterman.com/posts/making-waterdeep-dragon-heist-your-own</link><guid isPermaLink="true">https://ajkueterman.com/posts/making-waterdeep-dragon-heist-your-own</guid><description>Supplements and ideas for your next sprawling urban campaign.</description><pubDate>Sun, 20 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://dnd.wizards.com/products/tabletop-games/rpg-products/dragonheist&quot;&gt;&lt;em&gt;&lt;strong&gt;Waterdeep: Dragon Heist&lt;/strong&gt;&lt;/em&gt;&lt;/a&gt; is one of the most exciting adventure modules for D&amp;amp;D 5e, dropping your Level 1 adventuring party into the heart of the &lt;em&gt;&apos;City of Splendors&apos;&lt;/em&gt;. But how can you take this dense urban caper and make it your own?&lt;/p&gt;
&lt;p&gt;Waterdeep is one of the most well known cities in the Forgotten Realms, positively brimming with adventure and backed up with heaps and heaps of lore. In the book there is an entire appendix - &lt;em&gt;&quot;Volo’s Waterdeep Enchiridion&quot;&lt;/em&gt; - which introduces the DM and adventurers to the city in a crash course that covers history, locations and getting around, and special events &amp;amp; points of interest.&lt;/p&gt;
&lt;p&gt;All of this is extremely helpful to those new to Waterdeep, while simultaneously being &lt;em&gt;completely overwhelming&lt;/em&gt;. Not only do you have to try to internalize some of this lore to have it ready at the table when you offer the occasional History check, but you probably also want to try to populate Waterdeep in a way that makes it feel as big as it&apos;s described when your players are moving around the world.&lt;/p&gt;
&lt;p&gt;Here are my tips for running &lt;em&gt;&lt;strong&gt;Waterdeep: Dragon Heist&lt;/strong&gt;&lt;/em&gt;, along with the most useful sources &amp;amp; supplements I&apos;ve gone to along the way.&lt;/p&gt;
&lt;h2&gt;Buy the Right Sources&lt;/h2&gt;
&lt;p&gt;Before you can get started running your game, you need to acquire the right materials. But before you start shelling out that cold hard cash, you need to consider the game you plan to run.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/waterdeep/dnd-minis-dragon.jpeg&quot; alt=&quot;dragon mini on game board&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you&apos;re running the game in-person&lt;/strong&gt;, then the &lt;a href=&quot;https://dnd.wizards.com/products/tabletop-games/rpg-products/dragonheist&quot;&gt;adventure book&lt;/a&gt; is indispensible. Having that reference on-hand during the game is super useful. In my experience quickly flipping through the pages of the adventure book is much more effective even than swapping tabs and searching the text of a digital book.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you&apos;re running the game remotely&lt;/strong&gt;, then the physical book is a luxury, not a necessity. Instead, you need to decide on one or more digital versions of the module. Your two options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Buy the adventure on &lt;a href=&quot;https://marketplace.roll20.net/browse/bundle/3825/waterdeep-dragon-heist&quot;&gt;Roll20&lt;/a&gt; or your favorite VTT (Virtual Tabletop) of choice.&lt;/li&gt;
&lt;li&gt;Purchase the adventure source on &lt;a href=&quot;https://www.dndbeyond.com/sources/wdh&quot;&gt;D&amp;amp;D Beyond&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Roll20 (or VTT)&lt;/h4&gt;
&lt;p&gt;In general, I would strongly suggest buying the VTT version of the adventure. On Roll20, purchasing the adventure gives you access to the entire text of the adventure &lt;em&gt;plus&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of the maps included in the adventure, pre-built with the proper size, layers, and dynamic lighting.&lt;/li&gt;
&lt;li&gt;All of the key NPC&apos;s, monsters, and other tokens populated in the VTT with character sheets &amp;amp; abilities.&lt;/li&gt;
&lt;li&gt;The adventure structure broken out by season, so you get only the versions of the map you need, pre-populated with the correct monster tokens &amp;amp; lighting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features add up to hours of prep time saved, meaning you have much more time to focus on the story you want to tell, or by enhancing your Roll20 game with additional maps &amp;amp; characters.&lt;/p&gt;
&lt;h4&gt;D&amp;amp;D Beyond&lt;/h4&gt;
&lt;p&gt;So if you&apos;ve already decided to pick up the Roll20 Marketplace version for your remote game, or the physical book for your in-person game - then why would you turn to D&amp;amp;D Beyond?&lt;/p&gt;
&lt;p&gt;The primary reason the D&amp;amp;D Beyond sources are valuable, is it opens up the possibility of sharing some content with your campaign. For certain campaigns, this can mean unique character backgrounds that let players customize their characters to this specific adventure.&lt;/p&gt;
&lt;p&gt;In the case of &lt;em&gt;Waterdeep: Dragon Heist&lt;/em&gt;, there aren&apos;t unique backgrounds to this adventure. There are, however, especially relevant backgrounds in the &lt;a href=&quot;https://www.dndbeyond.com/sources/scag&quot;&gt;&lt;em&gt;Sword Coast Adventurer&apos;s Guide&lt;/em&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Players looking for background options beyond those described in the Player’s Handbook can find several appropriate ones in the &lt;em&gt;&lt;strong&gt;Sword Coast Adventurer’s Guide&lt;/strong&gt;&lt;/em&gt;: City Watch, cloistered scholar, courtier, faction agent, far traveler, inheritor, mercenary veteran, urban bounty hunter, and Waterdavian noble. If you have access to this book, consider making its background options available to your players’ characters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So if you can avoid spending the extra &lt;a href=&quot;https://forgottenrealms.fandom.com/wiki/Dragon_(coin)&quot;&gt;dragons&lt;/a&gt; on the D&amp;amp;D Beyond source, it may be more worthwhile to invest in the &lt;a href=&quot;https://www.dndbeyond.com/sources/scag&quot;&gt;&lt;em&gt;Sword Coast Adventurer&apos;s Guide&lt;/em&gt;&lt;/a&gt;, which will give you much more background and context relevant to the region and &lt;em&gt;also&lt;/em&gt; give you those extra backgrounds.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.dmsguild.com/product/251816/Waterdeep-City-Encounters&quot;&gt;Waterdeep: City Encounters&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So you unfurl the poster map of Waterdeep to see the insane level of detail in this city and your brain is probably reeling as you imagine what to fill it with. You may have some ideas for a spare NPC or side quest, but figuring out hooks for this whole world is a daunting task.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/waterdeep/city-encounters.png&quot; alt=&quot;children cheer on the griffon calvary flying above the streets of waterdeep&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enter &lt;a href=&quot;https://www.dmsguild.com/product/251816/Waterdeep-City-Encounters&quot;&gt;&lt;em&gt;Waterdeep: City Encounters&lt;/em&gt;&lt;/a&gt;, an amazing supplement for any Waterdeep-based campaign or Sword Coast adventure that includes the City of Splendors. Brought to you by an amazing team of authors &amp;amp; Guild Adepts on the &lt;a href=&quot;https://www.dmsguild.com/&quot;&gt;Dungeon Masters Guild&lt;/a&gt;, this supplement offers over &lt;em&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;/em&gt; encounters that are set throughout the city.&lt;/p&gt;
&lt;p&gt;The module offers two flavors of encounters - a table of random encounters that change based on what ward your characters are currently exploring, and a dense set of location-based encounters.&lt;/p&gt;
&lt;p&gt;This design means you can sprinkle random encounters throughout your campaign no matter where your characters are in the city, spicing up any possibly dull moments or just giving your players a sense of life happening in the background within the walls of Waterdeep. With the location-based encounters, you can make the city&apos;s points of interest really stand out and be memorable to your players, or offer an opportunity to draw them into a larger plot or side quest.&lt;/p&gt;
&lt;p&gt;If you&apos;re worried about the encounters being too brief - the module offers a good balance. Some encounters are complete one-offs, but others connect to one another, and can unfurl larger plots that connect locations throughout the city. Some encounters also pull in notable Waterdavian characters that are also involved in &lt;em&gt;Dragon Heist&lt;/em&gt;, offering opportunities to weave the two narratives together.&lt;/p&gt;
&lt;p&gt;Overall, I think &lt;em&gt;Waterdeep: City Encounters&lt;/em&gt; is a must-have supplement to &lt;em&gt;Dragon Heist&lt;/em&gt;, or any Sword Coast-based adventure that might draw your characters here.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.dmsguild.com/product/252855/Blue-Alley&quot;&gt;Blue Alley&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This adventure can be seen as a prologue to the epic dungeon crawl that is &lt;a href=&quot;https://dnd.wizards.com/products/tabletop-games/rpg-products/waterdeep-dungeon-mad-mage&quot;&gt;&lt;em&gt;Waterdeep: Dungeon of the Mad Mage&lt;/em&gt;&lt;/a&gt;. Despite that, &lt;em&gt;Dragon Heist&lt;/em&gt; is actually somewhat light on dungeon-crawling. The structure is a bit more encounter-based, allowing your characters to explore and bounce around as they unfurl the plot.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/waterdeep/blue-alley-cover.png&quot; alt=&quot;blue alley cover&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So if you&apos;re looking for a way to spice up the side-quests for your characters and offer some extremely solid dungeon-crawling, you need to pick up &lt;a href=&quot;https://www.dmsguild.com/product/252855/Blue-Alley&quot;&gt;&lt;em&gt;&lt;strong&gt;Blue Alley&lt;/strong&gt;&lt;/em&gt;&lt;/a&gt; on the DM&apos;s Guild.&lt;/p&gt;
&lt;p&gt;In &lt;em&gt;Blue Alley&lt;/em&gt;, your adventurers are tempted with gold &amp;amp; glory by none other than &lt;a href=&quot;https://forgottenrealms.fandom.com/wiki/Mirt&quot;&gt;Mirt&lt;/a&gt; - offering you an easy hook into &lt;em&gt;Dragon Heist&lt;/em&gt;, especially if your characters have forged a connection with &lt;a href=&quot;https://forgottenrealms.fandom.com/wiki/Harpers&quot;&gt;The Harpers&lt;/a&gt;. The dungeon offers an array of traps, puzzles, monsters, and treasures to unfold. It is unique and varied in its challenges, and should offer possibilities for further side-quest hooks if you want to get creative.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://www.dmsguild.com/product/348057/Blue-Alley-Map&quot;&gt;Blue Alley Map&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;If you want to spice up your dungeon-dive with some amazing improved maps, I also highly recommend the &lt;a href=&quot;https://www.dmsguild.com/product/348057/Blue-Alley-Map&quot;&gt;&lt;em&gt;Blue Alley&lt;/em&gt; Map&lt;/a&gt; supplement as well. They are complete with beautiful lighting &amp;amp; improved assets to help spruce up your VTT version of the game.&lt;/p&gt;
&lt;h2&gt;Maps&lt;/h2&gt;
&lt;p&gt;If you&apos;re running the game in-person, a &lt;a href=&quot;https://www.amazon.com/dp/B0742H1FJ1?ref_=cm_sw_r_cp_ud_dp_S2WEPWWQS1E7DSTWP9TB&quot;&gt;grid board&lt;/a&gt; and &lt;a href=&quot;https://www.amazon.com/EXPO-Vis-%C3%A0-Vis-Markers-Assorted-Colors/dp/B08QSK3RH4&quot;&gt;wet erase markers&lt;/a&gt; are your absolute best friend. However if you&apos;re running the game on a virtual table top and are looking for ways to expand on the maps included in the adventure, check out a few of these excellent mapping resources.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/waterdeep/fantasy-map.jpeg&quot; alt=&quot;fantasy map&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://dysonlogos.blog/&quot;&gt;Dyson Logos&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The sole map illustrator for &lt;em&gt;Dragon Heist&lt;/em&gt; is &lt;a href=&quot;https://dysonlogos.blog/about/&quot;&gt;Dyson Logos&lt;/a&gt; - a legendary RPG map-maker with hundreds of free maps and many more resources for patrons, plus full books of maps for sale online.&lt;/p&gt;
&lt;p&gt;If you&apos;re interested in expanding on the existing maps in this module, his website is a great resource for random battle maps and maps to inspire fun side quests or mini-dungeons. A few sections on his website highlight some especially useful categories, including &lt;a href=&quot;https://dysonlogos.blog/maps/inns-and-taverns/&quot;&gt;Inns and Taverns&lt;/a&gt; and &lt;a href=&quot;https://dysonlogos.blog/maps/sewers/&quot;&gt;Sewers&lt;/a&gt;, that blend easily into the urban setting.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://www.czepeku.com/&quot;&gt;Cze and Peku Maps&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Another great D&amp;amp;D map-making site, Cze and Peku make beautiful maps for all sorts of games. From the Astral Sea to an encounter with an Elder being to a lovely day by the lake shore, they have drawn a map for any situation.&lt;/p&gt;
&lt;p&gt;Simply &lt;a href=&quot;https://www.czepeku.com/search?q=city&quot;&gt;searching by the &apos;city&apos; keyword&lt;/a&gt; brings up dozens of amazing maps that would slot easily into &lt;em&gt;Dragon Heist&lt;/em&gt;. And at ~$5 a map they can be an affordable way to expand your version of Waterdeep and add that level of immersion we&apos;re all searching for.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://dicegrimorium.com/&quot;&gt;Dice Grimorium&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;If you&apos;re looking for a bank of free, full-color battle maps to have at the ready for any encounter, the &lt;a href=&quot;https://dicegrimorium.com/free-rpg-map-library/&quot;&gt;Dice Grimorium free catalog&lt;/a&gt; is just that.&lt;/p&gt;
&lt;p&gt;George works to create a new map each week, most of which he amazingly shares for free on his site and social media. In addition you can support him on Patreon and get additional tokens for each map (perfect for your VTT) and even the original PSD files in case you have map-customization aspirations.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;As a DM, you can never do enough preparation. I hope this guide has helped give you some resources, ideas, or inspiration for your next adventure to the &lt;em&gt;City of Splendors&lt;/em&gt;. Don&apos;t let this beautiful gem of the Sword Coast intimidate you, and make it into what you want.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/waterdeep/d20-amber.jpeg&quot; alt=&quot;d20 on table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As always, the act of DM&apos;ing is more an art than a science. Remember to improvise. Remember to let your players lead. And remember to have fun!&lt;/p&gt;
&lt;p&gt;Good luck out there! If you have thoughts, feedback, or suggestions for future posts - let me know &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;on Mastodon&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Use Good Tools</title><link>https://ajkueterman.com/posts/use-good-tools</link><guid isPermaLink="true">https://ajkueterman.com/posts/use-good-tools</guid><description>A quick thought on dev tools, and how they make us all better.</description><pubDate>Tue, 27 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently finished reading &lt;a href=&quot;https://www.amazon.com/Blood-Sweat-Pixels-Triumphant-Turbulent/dp/0062651234&quot;&gt;Blood, Sweat, and Pixels&lt;/a&gt; by Jason Schreier, a book that goes behind the scenes on the making of several popular video games released in the last decade. The book was an awesome look at the drama inherent in a massive creative endeavor like making games or movies, and it had some interesting parallels to other fields - including software development.&lt;/p&gt;
&lt;p&gt;One of my favorite quotes in the book was specifically insightful, and relatable from my experience writing applications. When discussing the writing of the engine &amp;amp; tooling behind &lt;a href=&quot;https://en.wikipedia.org/wiki/Destiny_(video_game)&quot;&gt;Destiny&lt;/a&gt;, one developer said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot;The biggest differentiator between a studio that creates a really high quality game and a studio that doesn&apos;t isn&apos;t the quality of the team, it&apos;s their dev tools. If you can take 50 shots on goal and you&apos;re a pretty shitty hockey player, and I can only take 3 shots on goal and I&apos;m Wayne &lt;strong&gt;fucking&lt;/strong&gt; Gretzky, you&apos;re probably going to do better. That&apos;s what tools are. It&apos;s how fast can you iterate.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I immediately flashed back to all the dev tools I&apos;ve worked with over the years. How something like moving from Eclipse to Android Studio completely changed the Android Dev experience and made it accessible in a way it wasn&apos;t before. I thought about some of the rockstar devs I worked with who moved mountains to ship good code with bad tools. While that was impressive it hid the fact that, given good tools, maybe everyone on the team could have been putting out similar work.&lt;/p&gt;
&lt;p&gt;Invest in good dev tools. Listen to new people who join the team and have complaints about the way that you work. Building or buying better dev tools is a more sustainable way to get more good work out there, because it doesn&apos;t rely on only the &lt;a href=&quot;/posts/thoughts-on-10x-engineers&quot;&gt;&lt;em&gt;&quot;best&quot;&lt;/em&gt;&lt;/a&gt;, most technical people to make your teams successful.&lt;/p&gt;
&lt;p&gt;Sure, we want devs who can overcome hurdles. But if you rely on only hiring the devs who can survive your bad dev pipeline to ship code, you&apos;re going to have a lot of people not shipping, because they&apos;re stuck helping other devs learn to ship within a broken process.&lt;/p&gt;
&lt;p&gt;Instead of expecting people to reach a certain height to survive and ship good code, &lt;em&gt;raise the floor&lt;/em&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Android ViewModel - Manual Dependency Injection Made Easy</title><link>https://ajkueterman.com/posts/android-viewmodel-manual-dependency-injection-made-easy</link><guid isPermaLink="true">https://ajkueterman.com/posts/android-viewmodel-manual-dependency-injection-made-easy</guid><description>Remove some boiler plate and make manual dependency injection with Android View Models easier with the power of Kotlin extensions.</description><pubDate>Mon, 28 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The Android team has been increasingly vocal about their support for &lt;a href=&quot;https://en.wikipedia.org/wiki/Dependency_injection&quot;&gt;Dependency Injection&lt;/a&gt; frameworks like &lt;a href=&quot;https://dagger.dev/&quot;&gt;Dagger&lt;/a&gt;, going so far as to develop and recommend &lt;a href=&quot;https://developer.android.com/training/dependency-injection/hilt-android&quot;&gt;Hilt&lt;/a&gt; - their Android DI framework built on top of Dagger - for modern Android development.&lt;/p&gt;
&lt;p&gt;In their guide to &lt;a href=&quot;https://developer.android.com/training/dependency-injection/manual&quot;&gt;manual dependency injection&lt;/a&gt; the Android team lays out approaches to manual DI for View Models. They offer both the basic approach to manual DI - just instantiating everything you need in &lt;code&gt;onCreate&lt;/code&gt; and using &lt;code&gt;lateinit var&lt;/code&gt; View Models - and the container approach using a custom &lt;code&gt;AppContainer&lt;/code&gt; to handle dependencies across all your Activities.&lt;/p&gt;
&lt;p&gt;The alternative they give to this boiler-plate-heavy approach is to recommend Dagger or Hilt to handle this process for you. However, in many apps, pulling in a DI framework is overhead you really don&apos;t need. Instead, what if there was a way to manually inject dependencies into your Android Activity &amp;amp; Fragment View Models without all the boiler plate?&lt;/p&gt;
&lt;h2&gt;Lazy DI Using Android Lifecycle ViewModel Extensions&lt;/h2&gt;
&lt;p&gt;One of the ways  the Android Fragment &amp;amp; Lifecycle teams have tried to make the View Model easier to use in Activities and Fragments is providing the &lt;a href=&quot;https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel-ktx&quot;&gt;Android Lifecycle ViewModel Kotlin Extensions&lt;/a&gt; library.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies { 
    implementation &quot;androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This library lets you instantiate a View Model in a Fragment or Activity with a delegate method - making it easy to create a properly scoped View Model.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyActivity : AppCompatActivity() {

    private val model: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        model.getUsers().observe(this, Observer&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;{ users -&amp;gt;
            // update UI
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is beautiful! However, what if our View Model needs to use a &lt;a href=&quot;https://developer.android.com/jetpack/guide#overview&quot;&gt;repository&lt;/a&gt; to make a call to &lt;a href=&quot;https://square.github.io/retrofit/&quot;&gt;Retrofit&lt;/a&gt;? We would like to inject that repository into our View Model when we construct it.&lt;/p&gt;
&lt;h2&gt;View Model Provider Factory&lt;/h2&gt;
&lt;p&gt;One way to enable this behavior is to use &lt;a href=&quot;https://developer.android.com/reference/androidx/lifecycle/ViewModelProvider.Factory&quot;&gt;&lt;code&gt;ViewModelProvider.Factory&lt;/code&gt;&lt;/a&gt;, with which you can instantiate a View Model with it&apos;s needed dependencies. To do so, you need to create your own Factory that extends the &lt;code&gt;ViewModelProvider.Factory&lt;/code&gt; interface, or create a function that can return an &lt;code&gt;object&lt;/code&gt; that overrides the &lt;code&gt;Factory.create&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;If we wanted to stop here, we could create a Kotlin function to do this for us:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun createWithFactory(
  create: () -&amp;gt; ViewModel
  ): ViewModelProvider.Factory {
    return object : ViewModelProvider.Factory {
        override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
            @Suppress(&quot;UNCHECKED_CAST&quot;)// Casting T as ViewModel
            return create.invoke() as T
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And use it in our Activity or Fragment like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private val model: MyViewModel by lazy {
  ViewModelProvider(
    this, 
    createWithFactory {
        MyViewModel(repo = MyRepository())
    }
  ).get(MyViewModel::class.java)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now create a View Model with the necessary dependencies! However, even with our Factory abstracted into a function, we still need to manage the boiler plate of  &lt;code&gt;ViewModelProvider&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;View Model Provider &amp;amp; Kotlin Extension Mashup&lt;/h2&gt;
&lt;p&gt;Remember, we could use &lt;code&gt;by viewModels&lt;/code&gt; or &lt;code&gt;by activityViewModels&lt;/code&gt; to delegate the creation of our View Models, but we weren&apos;t able to inject our required dependencies without a Factory.&lt;/p&gt;
&lt;p&gt;Now that we have a handy way to instantiate a View Model with a Factory, we can let these Kotlin Extensions for View Model to hide this &lt;code&gt;ViewModelProvider&lt;/code&gt; boiler plate. Here&apos;s how it would look in a Fragment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private val model: MyViewModel by activityViewModels {
    createWithFactory {
        MyViewModel(repo = MyRepository())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Almost perfect. All we have to do is provide a Factory and a lambda that returns our View Model. But, with a little help from the &lt;code&gt;lifecycle-viewmodel-ktx&lt;/code&gt; library and Kotlin extension functions, we can take it one step further.&lt;/p&gt;
&lt;h2&gt;Endgame: Easy View Model DI&lt;/h2&gt;
&lt;p&gt;Let&apos;s use the power of extension functions, our Factory knowledge, and the &lt;code&gt;ViewModelLazy&lt;/code&gt; class to delegate the creation of our View Model to functions on Activity and Fragment.&lt;/p&gt;
&lt;p&gt;We provide the function to create our View Model as a lambda that returns type &lt;code&gt;&amp;lt;VM : ViewModel&amp;gt;&lt;/code&gt;, and the &lt;code&gt;viewModelBuilder&lt;/code&gt; and &lt;code&gt;activityViewModelBuilder&lt;/code&gt; extensions do the rest.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Get a [ViewModel] in an [ComponentActivity].
 */
@MainThread
inline fun &amp;lt;reified VM : ViewModel&amp;gt; ComponentActivity.viewModelBuilder(
    noinline viewModelInitializer: () -&amp;gt; VM
): Lazy&amp;lt;VM&amp;gt; {
    return ViewModelLazy(
        viewModelClass = VM::class,
        storeProducer = { viewModelStore },
        factoryProducer = {
            return@ViewModelLazy object : ViewModelProvider.Factory {
                override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
                    @Suppress(&quot;UNCHECKED_CAST&quot;)// Casting T as ViewModel
                    return viewModelInitializer.invoke() as T
                }
            }
        }
    )
}

/**
 * Get a [ViewModel] in a [Fragment].
 */
@MainThread
inline fun &amp;lt;reified VM : ViewModel&amp;gt; Fragment.activityViewModelBuilder(
    noinline viewModelInitializer: () -&amp;gt; VM
): Lazy&amp;lt;VM&amp;gt; {
    return ViewModelLazy(
        viewModelClass = VM::class,
        storeProducer = { requireActivity().viewModelStore },
        factoryProducer = {
            object : ViewModelProvider.Factory {
                override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
                    @Suppress(&quot;UNCHECKED_CAST&quot;)// Casting T as ViewModel
                    return viewModelInitializer.invoke() as T
                }
            }
        }
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, in our Activity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private val model: MyViewModel by viewModelBuilder {
    MyViewModel(repo = MyRepository())  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or Fragment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private val model: MyViewModel by activityViewModelBuilder {
    MyViewModel(repo = MyRepository())  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can easily build a ViewModel, along with all it&apos;s necessary dependencies, all without the annoying boilerplate!&lt;/p&gt;
&lt;p&gt;If you&apos;re in a situation where you need complex Android View Models, want to follow SOLID principles of dependency injection, and you&apos;re not quite ready to adopt the complexity of a full Dependency Injection Framework - hopefully this helps present some other options that can make the task easier to manage.&lt;/p&gt;
&lt;p&gt;If you have thoughts on the Android View Model or want to share your approaches to manual DI, don&apos;t hesitate to reach out &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;on Mastodon&lt;/a&gt;, or follow me on &lt;a href=&quot;https://dev.to/robotsquidward&quot;&gt;DEV.to&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Library&lt;/h3&gt;
&lt;p&gt;If you just want to import and go, check out the &lt;a href=&quot;https://github.com/robotsquidward/lazyviewmodels&quot;&gt;Lazy ViewModels library on GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Lean Software Estimation</title><link>https://ajkueterman.com/posts/lean-software-estimation</link><guid isPermaLink="true">https://ajkueterman.com/posts/lean-software-estimation</guid><description>Estimation is one of the hardest problems in software development.</description><pubDate>Wed, 09 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Estimation is one of the hardest problems in software development. Trying to calculate the time required to solve complex technical and organizational problems is more magic than art. As &lt;a href=&quot;https://twitter.com/kvlly/status/1295805240692924417&quot;&gt;Kelly Vaughn&lt;/a&gt; on Twitter put it...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When you give a developer 5 hours for a task, they&apos;ll let you know they need 50 hours once they&apos;ve used up 10.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even looking at the most atomic of these calculations - estimating an engineering task or user story - it&apos;s clear to see that estimations are no exact science. We engineers size stories based on technical complexity, but even that is no exact correlation to time spent, as there are countless variables to this measure - skillset, experience, motivation, relationships - just to name a few.&lt;/p&gt;
&lt;p&gt;Now multiply that complexity by the number tasks or user stories required to make up a feature. Consider how those variables expand beyond one developer to a team, or group of teams as the complexity of the compounded problems build on each other. As features combine to form epics, and epics may be combined to form a product release, we start to spiral out of control.&lt;/p&gt;
&lt;p&gt;Just as a 1 point story means something completely different team-to-team, the same is true of user story comparison or epic comparison. Estimates on this order of magnitude are practically meaningless, as even the most seasoned teams are only just estimating - and on a much smaller scale. Remember the tweet: 5 could be 50 when you&apos;re standing in front of a pile of work. 5 months becomes 4 years.&lt;/p&gt;
&lt;p&gt;Excellent advice could be to &lt;a href=&quot;https://critter.blog/2020/08/20/plan-the-sprint-not-the-project/&quot;&gt;shut up and plan the sprint&lt;/a&gt;, remembering that our agile frameworks like Lean teach us to &lt;a href=&quot;https://en.wikipedia.org/wiki/Lean_software_development#Decide_as_late_as_possible&quot;&gt;make decisions late&lt;/a&gt; and focus on the most important things first. As an engineer, I know that is usually the right thing to do to be as efficient as possible. On the other hand, I&apos;m offloading a lot of the mental energy that it takes to decide &quot;what&apos;s next&quot; to our product teams.&lt;/p&gt;
&lt;p&gt;And in product, the eternal struggle is understanding how to plan. How to determine how to spend money and trying to calculate when investments are going to pay off. So the conversation turns to asking for commitments, and 2 weeks isn&apos;t a very long time.&lt;/p&gt;
&lt;p&gt;I fear the answer includes some nuance, but a few things I think you&apos;ll need first.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trust (and investment) in your dev teams&lt;/li&gt;
&lt;li&gt;Enable incremental improvement, and ask for proof&lt;/li&gt;
&lt;li&gt;Change as a result of tangible output, not perceived progress&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Trust (and Invest) in Your Dev Teams&lt;/h2&gt;
&lt;p&gt;The entire agile philosophy is built on the idea of high-performing teams. If you go into the process doubting that your dev team is performing at their best, or that they&apos;re not up to the task, the structure immediately begins to break down. When you don&apos;t trust a developer&apos;s estimate to be the truth - and instead some fake number meant to inflate the work or buy time - you&apos;ve failed.&lt;/p&gt;
&lt;p&gt;Even in the real scenario where your dev team isn&apos;t so high-performing, trusting their work, estimates, and convictions is critical to the process.&lt;/p&gt;
&lt;p&gt;To continue to build this trust, engineering teams should be invested in and &lt;a href=&quot;https://en.wikipedia.org/wiki/Lean_software_development#Empower_the_team&quot;&gt;empowered&lt;/a&gt;. It&apos;s the goal of the engineering leadership to find, cultivate, and empower great engineers. They should be experts at helping engineers reach their high-performing potential, and clear-eyed about areas to be improved. The culture in the engineering organization should be one that reinforces leadership and learning. &lt;strong&gt;Good engineers should be rewarded&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Enable Incremental Improvement, and Ask For Proof&lt;/h2&gt;
&lt;p&gt;The faster the product is in your hands, the faster you can start to learn. An unreleased product is a net drain on resources, whereas any code in production provides value to users and teams.&lt;/p&gt;
&lt;p&gt;This means that moving a product from development to production is more important than how much value you&apos;re providing per-release. Focus less on understanding the path to deliver on an entire feature or feature-set, and more on how you can start to iterate on the smallest changes possible. Let product ideas evolve instead of trying to decide on perfect early.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lean_software_development#Amplify_learning&quot;&gt;Working software is preferred over planning&lt;/a&gt;. Meaning your team should be focused on building. Each sprint, or faster, your engineering team will deliver software that can be demonstrated. Asking for real work to be demonstrated is a cornerstone of the process. Focus less on the scope of work and more on the fact that work can be delivered every sprint.&lt;/p&gt;
&lt;h2&gt;Change, as a Result of Output&lt;/h2&gt;
&lt;p&gt;The concept of &lt;a href=&quot;https://en.wikipedia.org/wiki/Lean_software_development#Decide_as_late_as_possible&quot;&gt;deciding late&lt;/a&gt; can be foreign to project teams used to slow, huge, disruptive releases. It&apos;s scary to &quot;let go and let Lean&quot; when we aren&apos;t used to seeing real code go fast.&lt;/p&gt;
&lt;p&gt;In a productive team, you should get used to letting things go to users un-polished, and learn how to react quickly to feedback. Try to resist the urge to change direction &lt;em&gt;before&lt;/em&gt; getting feedback from users.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Back to Kelly&apos;s tweet - you aren&apos;t going to be able to avoid engineers finding work that needs to be done. The point of the tweet is to focus on not putting too much time, effort, and value in zeroing in on a 5-hour estimate for something we don&apos;t understand yet.&lt;/p&gt;
&lt;p&gt;Instead, let engineers invest that planning time in writing real code. Let them learn from the code they do write. Let them get that code to production - fast! And then, change based on the code in users&apos; hands.&lt;/p&gt;
</content:encoded></item><item><title>Safely Launch Exception-Ready Coroutines</title><link>https://ajkueterman.com/posts/safely-launch-exception-ready-coroutines</link><guid isPermaLink="true">https://ajkueterman.com/posts/safely-launch-exception-ready-coroutines</guid><description>Handle exceptions from your Kotlin coroutines by default.</description><pubDate>Wed, 11 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Launching &lt;code&gt;suspend&lt;/code&gt; functions in Kotlin can be a complicated affair. Managing your &lt;code&gt;CoroutineScope&lt;/code&gt; and making sure exceptions are handled properly can be confusing and easy to forget.&lt;/p&gt;
&lt;p&gt;In Android, when using the &lt;code&gt;ViewModel&lt;/code&gt; or &lt;code&gt;Lifecycle&lt;/code&gt; specific scopes this gets much easier. We let the Android system provide a &lt;code&gt;CoroutineScope&lt;/code&gt; and manage killing our coroutines when the lifecycle of those things end.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun getObjectFromNetwork() {
  viewModelScope.launch {
    val response = networkRepository.getObject()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, there are cases when exceptions can be thrown from the &lt;code&gt;CoroutineScope&lt;/code&gt;. A real life example I experienced recently was a &lt;code&gt;SocketTimeoutException&lt;/code&gt; that was thrown from a Retrofit call I was making using a &lt;code&gt;suspend&lt;/code&gt; function. The result is &lt;a href=&quot;https://github.com/Kotlin/kotlinx.coroutines/issues/753&quot;&gt;an Android app crash&lt;/a&gt;, which is definitely not desired when network calls can result in many different thrown exceptions.&lt;/p&gt;
&lt;p&gt;The Kotlin &lt;a href=&quot;https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#coroutineexceptionhandler&quot;&gt;&lt;code&gt;CoroutineExceptionHandler&lt;/code&gt;&lt;/a&gt; can help us more easily handle exceptions thrown from our Coroutine scope, but it requires us to register the exception handler when we &lt;code&gt;launch&lt;/code&gt; a new coroutine so we &lt;a href=&quot;https://proandroiddev.com/managing-exceptions-in-nested-coroutine-scopes-9f23fd85e61&quot;&gt;properly handle nested exceptions&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
  // handle thrown exceptions from coroutine scope
  throwable.printStackTrace()
}

fun getObjectFromNetwork() {
  viewModelScope.launch(coroutineExceptionHandler) {
    val response = networkRepository.getObject()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, while the &lt;code&gt;ViewModel&lt;/code&gt; is probably a logical place for handling exceptions in network calls, there are a lot of exceptions that are thrown irregularly from your app that aren&apos;t part of the logical flow of a network call. Using Retrofit as an example, most network calls should return a &lt;a href=&quot;https://square.github.io/retrofit/2.x/retrofit/retrofit2/Response.html&quot;&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/a&gt; with a &lt;code&gt;body()&lt;/code&gt; or &lt;code&gt;errorBody()&lt;/code&gt; to handle instead of edge-cases where actual exceptions, like a &lt;code&gt;SocketTimeoutException&lt;/code&gt; is thrown. It might be worth it to you to abstract this error handling away from your &lt;code&gt;ViewModel&lt;/code&gt; to help minimize boiler plate.&lt;/p&gt;
&lt;p&gt;To try this out, let&apos;s leverage the power of Kotlin extensions to create a &lt;code&gt;safeLaunch&lt;/code&gt; method on &lt;code&gt;CoroutineScope&lt;/code&gt; that can apply a default &lt;code&gt;CoroutineExceptionHandler&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun CoroutineScope.safeLaunch(launchBody: suspend () -&amp;gt; Unit): Job {
  val coroutineExceptionHandler = CoroutineExceptionHandler { 
  coroutineContext, throwable -&amp;gt;
    // handle thrown exceptions from coroutine scope
    throwable.printStackTrace()
  }
  
  return this.launch(coroutineExceptionHandler) { 
    launchBody.invoke() 
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can call &lt;code&gt;safeLaunch&lt;/code&gt; on any &lt;code&gt;CoroutineScope&lt;/code&gt;, like our &lt;code&gt;viewModelScope&lt;/code&gt;, to launch a coroutine with this default error handling behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun getObjectFromNetwork() {
  viewModelScope.safeLaunch {
    val response = networkRepository.getObject()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There you have it! A nicely-encapsulated method to launch &lt;code&gt;suspend&lt;/code&gt; functions knowing that we won&apos;t see random app crashes because of an unhandled &lt;code&gt;Throwable&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If we wanted &lt;code&gt;safeLaunch&lt;/code&gt; to be the core &lt;code&gt;CoroutineScope&lt;/code&gt; launch method in our app, we can even improve the extension a bit to allow users the flexibility of passing their own &lt;code&gt;CoroutineExceptionHandler&lt;/code&gt; instead of using the default.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;   
  throwable.printStackTrace()
}

fun CoroutineScope.safeLaunch(
  exceptionHandler: CoroutineExceptionHandler = coroutineExceptionHandler,
  launchBody: suspend () -&amp;gt; Unit
): Job {  
  return this.launch(exceptionHandler) { 
    launchBody.invoke() 
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;The same day I wrote this article, &lt;a href=&quot;https://medium.com/@manuelvicnt&quot;&gt;Manuel Vivo&lt;/a&gt; and &lt;a href=&quot;https://medium.com/@florina.muntenescu&quot;&gt;Florina Muntenescu&lt;/a&gt; from the Android developer relations team released a really good series on coroutines, including the subject of coroutine exceptions. Check it out if you want to learn more about coroutines and how to manage them.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21&quot;&gt;Intro to Coroutines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629&quot;&gt;Cancelling Coroutines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c&quot;&gt;Exceptions in Coroutines&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>What Works at Work</title><link>https://ajkueterman.com/posts/what-works-at-work</link><guid isPermaLink="true">https://ajkueterman.com/posts/what-works-at-work</guid><description>A list of things that have worked better for me at work than other things.</description><pubDate>Sun, 19 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A list of things that have worked better for me at work than other things.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Worked&lt;/th&gt;
&lt;th&gt;Did Not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working in a shared space with my team worked.&lt;/td&gt;
&lt;td&gt;Being in an open office or in cubicles/offices did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working on sticky notes and whiteboards worked.&lt;/td&gt;
&lt;td&gt;Using a complex digital tool did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working all on-site together &lt;em&gt;or all distributed&lt;/em&gt; worked.&lt;/td&gt;
&lt;td&gt;Working with portions of the team remotely did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enabling developer collaboration worked.&lt;/td&gt;
&lt;td&gt;Forcing people to collaborate did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enabling pair programming worked.&lt;/td&gt;
&lt;td&gt;Forcing people to pair on everything did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing users to understand the problem worked.&lt;/td&gt;
&lt;td&gt;Assuming the problem or testing for the sake of testing did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working on small iterations of code worked.&lt;/td&gt;
&lt;td&gt;Thousand-line pull requests did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>A 2019 Freelance Year-in-Review</title><link>https://ajkueterman.com/posts/freelance-year-in-review-2019</link><guid isPermaLink="true">https://ajkueterman.com/posts/freelance-year-in-review-2019</guid><description>Summarizing a busy year of freelance work.</description><pubDate>Fri, 03 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2019 was an exciting year for me professionally, both in my career as an Android developer with Fifth Third Digital and also in my work with &lt;a href=&quot;https://base11studios.com/&quot;&gt;Base11 Studios&lt;/a&gt;. It is the first year I&apos;ve ever actually made a profit on doing freelance development work, and it was an exciting ride with a lot of lessons learned along the way. As 2019 comes to a close I wanted to take a minute just to share my experience and expand on some of those details.&lt;/p&gt;
&lt;p&gt;Base11 Studios is a business owned by two of my closest colleagues in the Cincinnati development scene, &lt;a href=&quot;https://twitter.com/rootbur&quot;&gt;Ryan Klein&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/itsdannyflash&quot;&gt;Dan Bellinski&lt;/a&gt;. Over the last two years I&apos;ve worked with them to help expand our catalogue of Base11 apps, both by writing code for Ryan &amp;amp; Dan projects and by building my own apps to release under Base11 Studios. As 2018 grew to a close last year - a busy year in which I launched both &lt;a href=&quot;https://base11studios.com/clients/fuzzzy/&quot;&gt;fuzZzy&lt;/a&gt; and &lt;a href=&quot;https://base11studios.com/clients/octonote/&quot;&gt;OctoNote&lt;/a&gt; on the App Store and built &lt;a href=&quot;https://base11studios.com/clients/fit-foods-coach/&quot;&gt;Fit Foods Coach&lt;/a&gt; for Android - we got comissioned to build a POC and later a full product on a scale unlike any previous Base11 Studios endeavor.&lt;/p&gt;
&lt;p&gt;So the story of 2019 is basically how I learned to deal with rapidly building a fully featured iOS app for clients, how that is different than building apps for yourself, how partnerships work in real life, and how you have to make hard choices when working in your free time - among other things.&lt;/p&gt;
&lt;h2&gt;From POC to Prod&lt;/h2&gt;
&lt;p&gt;In late 2018 and Dan, Ryan, and I worked on a POC prototype app to prove out image processing on iOS with an AWS back-end to detect specific types of documents. The focus was on capturing, processing, and predicting golf scores based on different types of scorecards.&lt;/p&gt;
&lt;p&gt;We had an amazing experience in this first leg of the journey. I learned a ton about iOS&apos;s Machine Learning and Vision API&apos;s and had the opportunity to put them into practice in a practical sense. Dan and Ryan built some real IP on the back end processing of scorecards and learned a ton about AWS, and specifically AWS products and strategy at our scale.&lt;/p&gt;
&lt;p&gt;At the end, we showed our clients our POC app and what we learned, and they decided they wanted to move forward with us to build out a production-ready version of our core feature set - scanning a golf scorecard and automatically detecting and calculating a handwritten score. Still minute in the types of startup dollars you hear about - this would be our biggest financial deal to date. The deal was structured as monthly payments to the LLC to build the prod-ready MVP app with a goal delivery date, at which time we&apos;d negotiate our partnership.&lt;/p&gt;
&lt;h2&gt;Building Fast&lt;/h2&gt;
&lt;p&gt;In this first sprint we worked from January until early May on delivering our prod-ready MVP. This was an exciting time filled with a lot of coding in my free time. I took a deep dive on the Vision API&apos;s I implemented in the prototype and completely rewrote the core scanning code to be performant. I built out the image manipulation portion of the code to re-shape images to the correct perspective based on camera/scorecard angle at capture. I overhauled and re-designed the entire UI, and implemented some inventive UI patterns to seamlessly work the user through the scanning, capture, and correction flow.&lt;/p&gt;
&lt;p&gt;At the end, the three of us were completely wiped, but excited with all that we had learned and built. We presented our MVP to our clients, who were extremely excited to move forward to release. We gave them a TestFlight build of the iOS app and a sandbox IST environment to test on, and planned to meet up to plan our rollout.&lt;/p&gt;
&lt;h2&gt;Goal Alignment&lt;/h2&gt;
&lt;p&gt;A big lesson from this first MVP handover was one of expectation and goal alignment. When we met with our clients to demo our MVP, we were 99% of the way to clicking the release-to-prod button in App Store Connect with our simple but production-ready app. To them, while they might have agreed on the production ready portion, they weren&apos;t ready to release our simplified effort to the wild world.&lt;/p&gt;
&lt;p&gt;After testing our app in the real world, and running into inevitable problems with image capture in a real-life environment, they were not ready to commit to a prod release. While I had already made big improvements in image capture between our MVP reveal and next meeting, they seemed to have other fundamental problems with the scope of our first MVP.&lt;/p&gt;
&lt;p&gt;So, after this first false start, we continued to work on tweaking and improving our MVP in our lead-up to an official meeting to plan our next few months of building towards their vision for an initial release.&lt;/p&gt;
&lt;h2&gt;Own Your Work&lt;/h2&gt;
&lt;p&gt;The meeting turned out to be one that defined our experience building this app, and tought us a hard lesson about owning our own IP in any project we built moving forward.&lt;/p&gt;
&lt;p&gt;Before we met, Dan, Ryan, Cory - our business lead, and I all agreed that we were ready to work to bring this fully-featured app to production as partners with our clients. We had worked really hard on our core feature set, but understanding the effort ahead after this recalibration and seeing how much of our IP we were essentially giving away for free, we wanted to make sure we had some ownership of the project in the next phase. We came into this meeting with the goal to understand what our equity stake would be in this project/company and discuss what we would build and by when.&lt;/p&gt;
&lt;p&gt;Our clients had done similar thinking, but of course had a different perspective on ownership. In their minds, we had taken their money to build and in doing so had voided our claim to equity. Why should they give us stake now, after already investing so much in this product - a product that they had already paid for? We didn&apos;t disagree on the fact that we were paid, but we were staking our equity claim on our IP expertise and what we were about to deliver for them to finalize their fully-featured first release. To them, this calculation didn&apos;t work out, and weren&apos;t willing to meet us anywhere close to our equity goals.&lt;/p&gt;
&lt;p&gt;We left the meeting with an understanding to continue our current agreement for our final month of the initial SOW, but to recalculate our cost to deliver the rest of the app - no equity offered.&lt;/p&gt;
&lt;p&gt;While we transitioned to this more contractor relationship we couldn&apos;t help but have this bad taste in our mouth from the exchange. We had no one to blame but ourselves for our decision to take payment to build our app instead of opting for equity only - but we also poured more than twice the effort into this app than we ever billed with the expectation in our gut that we owned some part of what we were building.&lt;/p&gt;
&lt;p&gt;The lesson here is &lt;strong&gt;own your work&lt;/strong&gt; no matter what. Software is worth so much more than what companies pay to build it, so when you have a chance to bet on yourself - do it! If I was re-negotiating our initial deal in hindsight I would demand the price we had initially charged +PLUS a large stake of equity in the finished product.&lt;/p&gt;
&lt;h2&gt;Graceful Goodbyes&lt;/h2&gt;
&lt;p&gt;After a few more months of negotiating and planning for phase 2, we were exhausted and disenchanted with the process. In the back of our minds we had our &lt;a href=&quot;https://base11studios.com/portfolio/&quot;&gt;portfolio&lt;/a&gt; of apps that we actually owned and craved to maintain. We knew we would never have equity in the project, and any time we poured into the project would only be on a contractor basis. After some serious deliberation the team agreed that &lt;a href=&quot;https://base11studios.com/&quot;&gt;Base11 Studios&lt;/a&gt; and our apps that we own are our priority - and we walked away from the phase 2 work, handing over our code and accounts along with it. The separation was amicable, but disappointing.&lt;/p&gt;
&lt;p&gt;The entire process was a hard-learned lesson, one that I&apos;ll always remember as long as I do any freelance work.&lt;/p&gt;
</content:encoded></item><item><title>Quick Take on 10x Engineers</title><link>https://ajkueterman.com/posts/thoughts-on-10x-engineers</link><guid isPermaLink="true">https://ajkueterman.com/posts/thoughts-on-10x-engineers</guid><description>Unicorns are real. Don&apos;t sacrifice good engineers for a unicorn.</description><pubDate>Mon, 15 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Just a quick thought on the 10x discussion that is going viral on dev Twitter as I write this post.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My experience in professional software development still relatively brief. That said, I&apos;ve already seen the &apos;10;1&apos; productivity gap in real life.&lt;/p&gt;
&lt;p&gt;However, it hasn&apos;t revealed itself as one developer on a team who kicks ass and does the work of five other ordinary devs in terms of rough code output. Instead, the 10x engineers I&apos;ve seen are the ones who get shit done when it needs to be done. They&apos;re the superhero devs, the martyrs, the people who bend over backwards to ship code. They reach out across teams, coordinate with managers, product owners, release management, etc.&lt;/p&gt;
&lt;p&gt;Obviously, that&apos;s a huge red flag about working environment. There should be no need for a 10x engineer in an organization when everyone is empowered and supported. You shouldn&apos;t need one person to be your savior in order to ship code. Any qualified dev off the street should be able to join your team and start shipping code.&lt;/p&gt;
&lt;p&gt;To me, the 10x discussion is about startup founders &amp;amp; VC&apos;s trying to make their money go a long way by finding that savior dev they can use to build an entire product by themselves. I believe those people exist, and are obviously valuable, but it&apos;s not a sustainable way to build an organization or company. And, the 10x engineers aren&apos;t going to be the same people to build out that organization.&lt;/p&gt;
&lt;p&gt;Finding good engineers is obviously extremely important. How you do it is equally important. Don&apos;t sacrifice a 5x engineer hoping for a 10x engineer, and make sure you can keep anyone you hire.&lt;/p&gt;
</content:encoded></item><item><title>ASWebAuthenticationSession API Changes in iOS 13</title><link>https://ajkueterman.com/posts/ios13-authentication-services-changes</link><guid isPermaLink="true">https://ajkueterman.com/posts/ios13-authentication-services-changes</guid><description>Updates to OAuth in iOS 13+</description><pubDate>Fri, 05 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the biggest announcements at WWDC 2019 was the new &apos;&lt;a href=&quot;https://developer.apple.com/sign-in-with-apple/&quot;&gt;Sign In With Apple&lt;/a&gt;&apos; feature, where Apple will now provide an authentication email &amp;amp; password to apps on behalf of you and manage them securely using the iCloud Keychain.&lt;/p&gt;
&lt;p&gt;Apple continues to push these privacy-focused features especially around authentication to try to disrupt ubiquitous services like Facebook &amp;amp; Google OAuth that make login flows much easier for users - but at the cost of their data going into the hands of ad companies.&lt;/p&gt;
&lt;p&gt;As such, a lot of focus has been given to the Authentication services in iOS. We saw it begin last year with the move from auth services inside &lt;a href=&quot;https://developer.apple.com/documentation/safariservices&quot;&gt;&lt;code&gt;SafariServices&lt;/code&gt;&lt;/a&gt; to the dedicated &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices&quot;&gt;&lt;code&gt;AuthenticationServices&lt;/code&gt;&lt;/a&gt; APIs. This year, iOS 13 leverages &lt;code&gt;AuthenticationServices&lt;/code&gt; to enable the Sign In With Apple APIs, and continue to refine the sign in experience.&lt;/p&gt;
&lt;h2&gt;A Guide to Changes in &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession&quot;&gt;&lt;code&gt;ASWebAuthenticationSession&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Last year, I talked a bit about OAuth in iOS in &lt;a href=&quot;/posts/sfauthenticationsession-and-aswebauthenticationsession&quot;&gt;my post about Apple&apos;s move from &lt;code&gt;SFAuthenticationSession&lt;/code&gt; to &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt;&lt;/a&gt; and how I went about converting my iOS app to use the new API. This year, the &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; has some smaller tweaks to enhance the OAuth sign-in experience across iOS (including iPadOS) devices.&lt;/p&gt;
&lt;p&gt;If you&apos;re a developer already sweating about the huge array of changes and opportunities coming for developers in 2019, don&apos;t fret about this one, it&apos;s a small tweak purely for enhancing OAuth experiences across devices.&lt;/p&gt;
&lt;p&gt;To set the stage, let&apos;s look at a code snippet for &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; in iOS 12.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
var webAuthSession: ASWebAuthenticationSession?
//...
@available(iOS 12.0, *)
func getAuthTokenWithWebLogin() {

    let authURL = URL(string: &quot;https://github.com/login/oauth/authorize?client_id=&amp;lt;client_id&amp;gt;&quot;)
    let callbackUrlScheme = &quot;octonotes://auth&quot;

    self.webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in

        // handle auth response
        guard error == nil, let successURL = callBack else {
            return
        }

        let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == &quot;code&quot;}).first

        // Do what you now that you&apos;ve got the token, or use the callBack URL
        print(oauthToken ?? &quot;No OAuth Token&quot;)
    })
    
    // Kick it off
    self.webAuthSession?.start()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, the only thing we need to provide the &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; is the authentication &lt;code&gt;URL&lt;/code&gt;, a callback URL scheme, and a completion block that handles the result of the OAuth. The OS handles the rest - displaying an alert, launching a Web login flow, and dismissing.&lt;/p&gt;
&lt;p&gt;In iOS 13, as Apple continues to refine the multi-app experience for iOS and iPadOS, we now need to help the OS out when it&apos;s making the decision on where and how to display the OAuth Alert and Web login flow.&lt;/p&gt;
&lt;p&gt;To do that, we have to let the &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; know which window is presenting the OAuth request. This is done by implementing the &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/aswebauthenticationpresentationcontextproviding&quot;&gt;&lt;code&gt;ASWebAuthenticationPresentationContextProviding&lt;/code&gt;&lt;/a&gt; interface in your presenting View Controller.&lt;/p&gt;
&lt;p&gt;The presenting View Controller needs to implement the &lt;code&gt;ASWebAuthenticationPresentationContextProviding&lt;/code&gt; interface and return the relevant window in the &lt;code&gt;presentationAnchor&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LoginViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
    //...
    func presentationAnchor(for session: ASWebAuthenticationSession) -&amp;gt; ASPresentationAnchor {
        return self.view.window ?? ASPresentationAnchor()
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, when setting up our auth session, we need to specify our &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237232-presentationcontextprovider&quot;&gt;&lt;code&gt;presentationContextProvider&lt;/code&gt;&lt;/a&gt; delegate.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.webAuthSession?.presentationContextProvider = context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full updated method, now passing in a &lt;code&gt;ASWebAuthenticationPresentationContextProviding&lt;/code&gt; context (our presenting VC that implements the &lt;code&gt;ASWebAuthenticationPresentationContextProviding&lt;/code&gt; interface).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
var webAuthSession: ASWebAuthenticationSession?
//...
@available(iOS 13.0, *)
func getAuthTokenWithWebLogin(context: ASWebAuthenticationPresentationContextProviding) {

    let authURL = URL(string: &quot;https://github.com/login/oauth/authorize?client_id=&amp;lt;client_id&amp;gt;&quot;)
    let callbackUrlScheme = &quot;octonotes://auth&quot;

    self.webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in

        // handle auth response
        guard error == nil, let successURL = callBack else {
            return
        }

        let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == &quot;code&quot;}).first

        // Do what you now that you&apos;ve got the token, or use the callBack URL
        print(oauthToken ?? &quot;No OAuth Token&quot;)
    })
    
    // New in iOS 13
    self.webAuthSession?.presentationContextProvider = context
    
    // Kick it off
    self.webAuthSession?.start()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, your &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; now knows where and how to display your Web-based login flow, and will provide a consistent experience across devices!&lt;/p&gt;
&lt;h2&gt;Thoughts on OAuth&lt;/h2&gt;
&lt;p&gt;It has been an interesting experience tinkering with OAuth over the last two years. Right as I was learning the APIs Apple made a big move to new APIs, and this year it&apos;s clear how that change is enabling a better sign in experience for users.&lt;/p&gt;
&lt;p&gt;Now, in iOS 13, by observing the specific change in this small API, we can see the evolution of iOS to a multi-window experience.&lt;/p&gt;
&lt;p&gt;As I see this slow progression play out, I&apos;m beginning to wonder about the future of OAuth in iOS at all. Will web authentication be blacklisted altogether in favor of Sign In With Apple? Will Apple centralize auth in a way that allows me to authenticate with GitHub without going to GitHub services for tokens? Only time will tell, but the changes today can project the changes in the future - stay alert!&lt;/p&gt;
&lt;p&gt;If you have any thoughts/questions/predictions about OAuth and Web Authentication with iOS, don&apos;t hesitate to reach out to me &lt;a href=&quot;https://androiddev.social/@aj&quot;&gt;on Mastodon&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Web Authentication with iOS AuthenticationServices</title><link>https://ajkueterman.com/posts/sfauthenticationsession-and-aswebauthenticationsession</link><guid isPermaLink="true">https://ajkueterman.com/posts/sfauthenticationsession-and-aswebauthenticationsession</guid><description>A simple guide to OAuth in iOS 11+</description><pubDate>Thu, 07 Jun 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last week I started looking into doing OAuth for an iOS application to enable a simple GitHub authentication flow.  After my quick research, I discovered there are more than a couple ways to do this authentication, but the newest and easiest way in Swift was the &lt;a href=&quot;https://developer.apple.com/documentation/safariservices/sfauthenticationsession&quot;&gt;&lt;code&gt;SFAuthenticationSession&lt;/code&gt;&lt;/a&gt; that was just introduced in iOS 11.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SFAuthenticationSession&lt;/code&gt; offers an easy API to launch a Safari View Controller to a given sign-in url where the user can authenticate through the web.  When the OAuth service returns with the auth token, a simple completion handler is called to handle the response.  It&apos;s simple, secure, and really fast to implement.&lt;/p&gt;
&lt;p&gt;Here&apos;s my func to authenticate with my GitHub app &lt;a href=&quot;https://github.com/robotsquidward/octonote&quot;&gt;OctoNotes&lt;/a&gt; in Swift:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
var authSession: SFAuthenticationSession?
//...
func getAuthToken() {
    //OAuth Provider URL
    let authURL = URL(string: &quot;https://github.com/login/oauth/authorize?client_id=&amp;lt;client_id&amp;gt;&quot;)
    let callbackUrlScheme = &quot;octonotes://auth&quot;

    //Initialize auth session
    self.authSession = SFAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme,
                                                    completionHandler: { (callBack:URL?, error:Error?) in

        // handle auth response
        guard error == nil, let successURL = callBack else {
            return
        }

        let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == &quot;code&quot;}).first

        // Do what you now that you&apos;ve got the token, or use the callBack URL
        print(oauthToken ?? &quot;No OAuth Token&quot;)
    })

    //Kick it off
    self.authSession?.start()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Easy enough!  An alert gets displayed to the user prompting them to auth, Safari handles it with their input/with the data it already has, then returns you an authenticated token.&lt;/p&gt;
&lt;p&gt;But you&apos;ll probably guess if you looked at that link to the &lt;a href=&quot;https://developer.apple.com/documentation/safariservices/sfauthenticationsession&quot;&gt;&lt;code&gt;SFAuthenticationSession&lt;/code&gt; docs&lt;/a&gt; that this isn&apos;t the end of the story.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;ASWebAuthenticationSession&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;On Tuesday at WWDC, in the &quot;Automatic Strong Passwords and Security Code AutoFill&quot; session, Apple announced a new API for authenticating through the web in your iOS apps, and deprecated &lt;code&gt;SFAuthenticationSession&lt;/code&gt;.  Short life for this handy API.&lt;/p&gt;
&lt;p&gt;Not to fear! The replacement for &lt;code&gt;SFAuthenticationSession&lt;/code&gt; is &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession&quot;&gt;&lt;code&gt;ASWebAuthenticationSession&lt;/code&gt;&lt;/a&gt;, and the new API is very familiar.&lt;/p&gt;
&lt;p&gt;Instead of importing &lt;code&gt;SafariServices&lt;/code&gt;, start by importing the new &lt;code&gt;AuthenticationServices&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import AuthenticationServices
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then follow the same exact pattern to start a &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; and handle the callback to retrive your OAuth Token.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
var webAuthSession: ASWebAuthenticationSession?
//...
@available(iOS 12.0, *)
func getAuthTokenWithWebLogin() {

    let authURL = URL(string: &quot;https://github.com/login/oauth/authorize?client_id=&amp;lt;client_id&amp;gt;&quot;)
    let callbackUrlScheme = &quot;octonotes://auth&quot;

    self.webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in

        // handle auth response
        guard error == nil, let successURL = callBack else {
            return
        }

        let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == &quot;code&quot;}).first

        // Do what you now that you&apos;ve got the token, or use the callBack URL
        print(oauthToken ?? &quot;No OAuth Token&quot;)
    })
    
    // Kick it off
    self.webAuthSession?.start()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Familiar, eh?  The new &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; should enable you to securely authenticate on the web and future-proof your app for any security features involved in web-based login.  So while we weep for year-long API&apos;s, at least Apple is making the conversion a breeze.&lt;/p&gt;
</content:encoded></item></channel></rss>