<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sven Bendel on Medium]]></title>
        <description><![CDATA[Stories by Sven Bendel on Medium]]></description>
        <link>https://medium.com/@ubuntudroid?source=rss-4fcaefd1f0f9------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*HGxpx6AJR8OtjPL6cBF5bQ.png</url>
            <title>Stories by Sven Bendel on Medium</title>
            <link>https://medium.com/@ubuntudroid?source=rss-4fcaefd1f0f9------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 26 Jun 2026 21:27:07 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@ubuntudroid/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to generate structured data with GPT-3 and make it 10x more useful]]></title>
            <link>https://ubuntudroid.medium.com/how-to-generate-structured-data-with-gpt-3-and-make-it-10x-more-useful-6a32888b74d3?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/6a32888b74d3</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[chatgpt]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[gpt-3]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sun, 12 Feb 2023 13:39:20 GMT</pubDate>
            <atom:updated>2023-02-12T14:12:04.463Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xXJgciVT1hS20JDt" /><figcaption>Photo by <a href="https://unsplash.com/@swimstaralex?utm_source=medium&amp;utm_medium=referral">Alexander Sinn</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><em>Nowadays, everyone seems to do stuff with OpenAI’s GPT-3/ChatGPT. But how much more useful would it be, if OpenAI’s text completion APIs would return structured data, like JSON, instead of plain text?</em></p><p>Consider the following use case: you want to create a little app that uses the OpenAI GPT-3 API to generate fantasy characters for your role-playing crew — with background stories, attributes and everything.</p><p>Yep, that nerd stuff many of us developers love so much. 🧙 But this is for everyone, so stay with me. It will be short and sweet!</p><p>Now, let’s start with a simple prompt like this:</p><blockquote>Create a character for a fantasy roleplaying adventure, with background story, physical attributes, mental attributes and appearance.</blockquote><p>GPT-3’s answer would be something like this (obviously depending on temperature and other parameters):</p><blockquote>Name: Evelynne Shadowblade <br>Background: Evelynne was born into a family of skilled assassins, and was trained in the art of stealth and subterfuge from a young age. However, she grew disillusioned with the family’s morally questionable work, and struck out on her own as a rogue adventurer.<br>Physical attributes: Evelynne is a lithe and agile half-elf, with short black hair and piercing green eyes. She stands at 5&#39;7” and weighs 120 lbs. Her lithe build allows her to move quickly and quietly, and her elven heritage gives her heightened senses and reflexes.<br>Mental attributes: Evelynne is intelligent and cunning, with a quick wit and a sharp tongue. She is also fiercely independent and distrustful of authority figures, preferring to work alone or with a small, tight-knit group of allies.<br>Appearance: Evelynne wears tight-fitting leather armor that allows her to move silently and quickly. She carries a pair of curved daggers at her waist, and a quiver of poisoned arrows on her back. Her face is often obscured by a hood or mask, to further obscure her identity.</blockquote><p>That’s cool! 🥷</p><p>But let’s be honest, if I were to display so much text in an app, it would look rather unappealing, and overall I couldn’t do a whole lot with plain text in terms of app features.</p><p>So how can we make this better? Well, we would first have to bring it into a format that…</p><ol><li>is consistent across responses.</li><li>is easy to parse and store (even if the generated data would be in different languages than English).</li></ol><p>Now, plain text GPT-3 output isn’t very well suited for this kind of requirements out of the box.</p><p>Instead, let’s try to get the API to spit out some JSON with a structure we define in the prompt:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f5a6d69be87123cc5998f680019c0213/href">https://medium.com/media/f5a6d69be87123cc5998f680019c0213/href</a></iframe><p>And this is what we get from GPT-3:</p><pre>{<br>  &quot;name&quot;: &quot;Evelynne Shadowblade&quot;,<br>  &quot;background&quot;: &quot;Evelynne was born into a family of skilled assassins, and was trained in the art of stealth and subterfuge from a young age. However, she grew disillusioned with the family&#39;s morally questionable work, and struck out on her own as a rogue adventurer.&quot;,<br>  &quot;physical_attributes&quot;: &quot;Evelynne is a lithe and agile half-elf, with short black hair and piercing green eyes. She stands at 5&#39;7\&quot; and weighs 120 lbs. Her lithe build allows her to move quickly and quietly, and her elven heritage gives her heightened senses and reflexes.&quot;,<br>  &quot;mental_attributes&quot;: &quot;Evelynne is intelligent and cunning, with a quick wit and a sharp tongue. She is also fiercely independent and distrustful of authority figures, preferring to work alone or with a small, tight-knit group of allies.&quot;,<br>  &quot;appearance&quot;: &quot;Evelynne wears tight-fitting leather armor that allows her to move silently and quickly. She carries a pair of curved daggers at her waist, and a quiver of poisoned arrows on her back. Her face is often obscured by a hood or mask, to further obscure her identity.&quot;<br>}</pre><p>Not bad at all—already ticks all the boxes from the list above!</p><p>But we can do even better! In RPGs the loadout of a character plays a big role. So let’s put it into a separate JSON array.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b36a48731fdcb6edb74ac7988e85f80e/href">https://medium.com/media/b36a48731fdcb6edb74ac7988e85f80e/href</a></iframe><p>And, sure enough, GPT follows the instructions to the point:</p><pre>{<br>  &quot;name&quot;: &quot;Evelynne Shadowblade&quot;,<br>  &quot;background&quot;: &quot;Evelynne was born into a family of skilled assassins, and was trained in the art of stealth and subterfuge from a young age. However, she grew disillusioned with the family&#39;s morally questionable work, and struck out on her own as a rogue adventurer.&quot;,<br>  &quot;physical_attributes&quot;: &quot;Evelynne is a lithe and agile half-elf, with short black hair and piercing green eyes. She stands at 5&#39;7\&quot; and weighs 120 lbs. Her lithe build allows her to move quickly and quietly, and her elven heritage gives her heightened senses and reflexes.&quot;,<br>  &quot;mental_attributes&quot;: &quot;Evelynne is intelligent and cunning, with a quick wit and a sharp tongue. She is also fiercely independent and distrustful of authority figures, preferring to work alone or with a small, tight-knit group of allies.&quot;,<br>  &quot;appearance&quot;: &quot;Evelynne wears tight-fitting leather armor that allows her to move silently and quickly. She carries a pair of curved daggers at her waist, and a quiver of poisoned arrows on her back. Her face is often obscured by a hood or mask, to further obscure her identity.&quot;,<br>  &quot;loadout&quot;: {<br>    &quot;weapons&quot;: [<br>      {<br>        &quot;name&quot;: &quot;Curved Daggers&quot;,<br>        &quot;description&quot;: &quot;A pair of razor-sharp daggers with a slight curve to the blade. Ideal for close-range combat and stealthy kills.&quot;,<br>        &quot;dps&quot;: 25.0<br>      },<br>      {<br>        &quot;name&quot;: &quot;Poisoned Arrows&quot;,<br>        &quot;description&quot;: &quot;Arrows dipped in a potent toxin that paralyzes or kills the target. Ideal for ranged combat and setting traps.&quot;,<br>        &quot;dps&quot;: 15.0<br>      }<br>    ],<br>    &quot;potions&quot;: [<br>      {<br>        &quot;name&quot;: &quot;Invisibility Potion&quot;,<br>        &quot;description&quot;: &quot;A potion that renders the drinker invisible for a short period of time. Ideal for sneaking past guards and avoiding detection.&quot;,<br>        &quot;effect&quot;: &quot;Grants temporary invisibility.&quot;<br>      },<br>      {<br>        &quot;name&quot;: &quot;Healing Potion&quot;,<br>        &quot;description&quot;: &quot;A potion that restores a portion of the drinker&#39;s health. Ideal for quick healing in combat or after a tough battle.&quot;,<br>        &quot;effect&quot;: &quot;Restores 50 health.&quot;<br>      }<br>    ]<br>  }<br>}</pre><p>Obviously we could go much further here, but I think you get the point: Y<strong>ou can easily get OpenAI’s GPT-3 API to respond with a JSON using literally any schema you want!</strong></p><p>Every developer out there will know what this means: it opens up a whole new set of applications with a super low entry barrier. 🚀 So go, code the next generation of GPT-based applications before someone else does. Exciting times! 🤯</p><p>Let me finish with some gotcha’s:</p><ol><li>Define value types as strictly as possible, such as units (“use SI units”, “use metric units”, “use g instead of kg”), types (“make “dps” a Double”) and the likes.</li><li>Be prepared for incomplete JSONs if the token limit is reached. Split up single big requests into multiple smaller ones or tell the API to keep it short (literally!) to avoid this situation. Repeat requests if necessary.</li><li>Following up on the previous point: When attempting to parse the JSON modify your parser to be as lenient as possible. For example: the generated JSON will at times have added commas at the end of lists and for the last key/value pair in objects. So you either need to remove those superfluous commas or use a JSON5 parser which by design allows trailing commas and more.<br>Here is a possible implementation of such a trailing comma remover in Kotlin for your convenience:</li></ol><pre>private val trailingCommaInObjectRegex = Regex(&quot;&quot;&quot;,\s*\}&quot;&quot;&quot;)<br>private val trailingCommaInArrayRegex = Regex(&quot;&quot;&quot;,\s*]&quot;&quot;&quot;)<br><br>private fun String.removeSuperfluousTrailingComma(): String {<br>    return replace(trailingCommaInObjectRegex, &quot;}&quot;)<br>      .replace(trailingCommaInArrayRegex, &quot;]&quot;)<br>}</pre><p><em>If this helped you, and you are now successfully tapping JSONs from OpenAI’s APIs, consider fueling my brain with some black liquid, too. </em>☕ <em>Much appreciated!</em></p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*1LFaaJOnWU9L0G6_c_wRWQ.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6a32888b74d3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Yet another case for working remotely?]]></title>
            <link>https://ubuntudroid.medium.com/yet-another-case-for-working-remotely-a45ef8e5ae97?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/a45ef8e5ae97</guid>
            <category><![CDATA[work]]></category>
            <category><![CDATA[remote-working]]></category>
            <category><![CDATA[workplace]]></category>
            <category><![CDATA[work-from-home]]></category>
            <category><![CDATA[office]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Fri, 14 Oct 2022 11:34:00 GMT</pubDate>
            <atom:updated>2022-12-10T10:42:21.837Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6-G03kpkfE2tGH1c" /><figcaption>Photo by <a href="https://unsplash.com/@gewoonwouter?utm_source=medium&amp;utm_medium=referral">Wouter</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><em>I’ve been working remotely for 6 years straight now. And no matter what my co-founder thinks, I could surely </em><strong><em>never </em></strong><em>(?*) go back to regular office life.</em></p><p>Now, I’m sure you’re thinking: not another treaty on the benefits of working remotely from a privileged knowledge worker.</p><p>Well, in part you are right: as a developer I am privileged and always have been. But things might be different than you might picture them…</p><p>I’m not just a developer, I’m also founder and CTO of a company called whisp — we are the crew behind CLEAR, one of the most popular intermittent fasting apps on the app stores. <strong>And my co-founder and CEO is fiercly pro-office.</strong></p><p>So I’m also writing this in part as the second half of a picture both of us are trying to paint at whisp. But I’m also doing this as an attempt to understand the office lovers in our team.</p><p>whisp was founded as a remote-first company three years ago. My co-founder and I had been working together for about 6 years. First at a dating app company with a strong office culture, including the obligatory table football, billiard, an amazing office space, free lunches, lots of parties and meetups— just what you would expect from a hip start-up in the late 2010s. Later both of us went to work at a remote-first app-agency with employees all over Europe — here we could work whenever wherever we wanted as long as we got shit done.</p><p>At one point we huddled and came to the conclusion, that it was time to roll our own thing. Both of us being well into our thirties it seemed like catching the last train for founding a startup before potential employees would deem us too old to be cool, while at the same time having acquired enough experience to avoid the biggest traps in the app business.</p><p>So off we went. Apparently we had similar experiences from the past, at least when it came to the last 3 years where we had worked together — undeniably the most influential ones in our career up to that point. It seemed to be a given that we would have similar ideas for how we would want to work together at whisp.</p><p>At first it just looked like that. We gathered for quite a few internal hackathons to conceive the next big thing and in the end came up with a prototype for an intermittent fasting app — as intermittent fasting also had helped us a lot to cope with the stress (and unhealthy habits) of our previous gigs we thought it would be something other people might be interested as well.</p><p>Then we parted into our own spaces and started hacking on the real thing. Both of us are living in different places, roughly 60 kilometers apart. We knew how to efficiently work remotely, we didn’t have money for an office — working from home seemed like a given.</p><p>And so our company became remote-first — at first…</p><p>But soon my co-founder muttered. He is an extrovert par excellence, thriving on in-person contact, while I am what many would describe as the typical introverted developer — not being completely averse to social interaction, but only in small doses and with big breaks in between.</p><p>So working remotely obviously was more beneficial to me than to him. I enjoyed planning features with him via video call, spec’ing them out from a design perspective in Figma (that was mostly his part) and then technically in Slite and Linear (my part). Afterwards I went head-on into undisturbed development — keeping him posted about my progress in Linear and Slack, but apart from that focusing on doing what I can do best: 1000111110110110111001… (you get the idea)</p><p>At the same time my co-founder would go and search for investors and plan new features.</p><p>In the meantime we hired a handful of new employees, most on the product and content side of things, but also a few developers.</p><p>But while things really looked well on the business side, something felt off, especially for my co-founder. He felt a sense of disconnection. We hired many of our employees and freelancers on the notion of being a remote-first company. At the same time most of them came from our region as we had extensively sourced from our existing contacts from our former jobs.</p><p>So we decided to provide them with a place where they could work from if they didn’t want to work from home or didn’t have a suitable place there.</p><p>We went looking for a small office space in a shiny new coworking space. Working from there was purely optional.</p><p>After some time we observed something interesting: one half of the employees worked from the office nearly full-time. The other half worked from home as much as they could.</p><p>The company had become what my co-founder aptly once called <em>remocal</em>. Half working colocated and in-person, half working remotely. But could that work? What kind of team member went for which option? And why?</p><h3>Remoters</h3><p>As mentioned before, half of our company consists of people who strongly prefer working remotely over work from a shared office space — I call us (slightly unimaginatively) Remoters.</p><h4>Why?</h4><p>People who opted to work remotely most or all of the time (which includes me) usually cited the following reasons for doing this:</p><ul><li>A new-found freedom: many of them never had worked remote-first before. They especially enjoy to be able to choose time and location of their work.</li><li>Additional time for private matters, hobbies and family, as they don’t have to commute to and from work.</li><li>They enjoy the high levels of trust we put into them (when we founded the company we opted for not tracking time at all)</li><li>The abundance of focus time they are getting when working uninterrupted. We are keeping digital meetings to a minimum and try to work async as much as possible.</li><li>Out of reach from certain viruses — yes, COVID, I’m looking at you.</li></ul><h4>Who?</h4><p>This is by no means to be generalized, but in our company people preferring to work remotely tend to tick the following boxes:</p><ul><li>Rather introvert than extrovert. They enjoy social interaction, but only a limited amount with longer breaks to decompress.</li><li>Good at setting work/life boundaries and shutting off after work.</li><li>Good at planning, scheduling and productivity in general.</li><li>Very reliable, a core trait to make remote work possible. Most expect the same thing from their coworkers.</li><li>Need silence and their mode to get into the zone. You will see them with headphones on all time when they need to work in a space where other people are communicating. Sensory overload is a thing for this group. Most of them also will not be able to focus when someone is constantly looking over their shoulder.</li><li>Learn and improve on their own schedule. They usually prefer a self-paced online course over a three day workshop to get the hang of a new topic.</li><li>Have special requirements for their work setup, e.g. a warm cozy area, a cup of Joe made from their favorite carefully selected beans, a clicky keyboard unsuitable for shared office spaces (that would be me).</li><li>Some have very busy schedules and/or love/have to scatter their work stints over the course the day — parents will relate.</li><li>Some people in this group love to work from different places all over the world which is only feasible when working remotely.</li></ul><h3>Officionados</h3><p>Our office crew which you would find at least 80% of the time in our office values different things as they are a different sort of people. My co-founder is one of those Officionados.</p><h4>Why?</h4><p>For Officionados choosing the office as their preferred working location feels just logical due to the following reasons:</p><p>They needed the in-person meetings and water cooler talks as they considered it a vital part of their creating synergies — and bonding. Also, working on a problem in an office face-to-face can help pushing through difficulties as encouragement comes easier when being in the same room.</p><ul><li>A feeling of connectedness. This group loves to bond via social interactions. Please don’t mistake this with loud people, they may be silent at times, but they are not breaking a sweat when facing a new person and have to do small talk.</li><li>Synergies which often unfold over water-cooler talks. Unbelievable things can grow out of those unplanned meetings and Officionados value this a lot.</li><li>Clear separation of work and (private) life — when at the office it’s work time, when not, private life unfolds.</li><li>High quality and work suited space. Not everyone has access to a perfectly equipped home office, let alone a separate room for work only. And not everyone can switch off at home when there are children, partners or roommates around. A dedicated office solves these problems.</li><li>The maker spirit of co-working spaces. As opposed to exclusive offices, offices and work stations in co-working spaces tend to have many different kinds of interesting people with tons of inspiring ideas and characters floating around. Certainly not the worst place for a startup-team to be.</li></ul><h4>Who?</h4><ul><li>Learn and grow with people, not documents. They learn most efficiently in a group, when pushing each other through motivation valleys and to the limits when approaching the apex.</li><li>Love to hang around with their colleagues after work. This obviously is possible with much less ceremony when simply leaving work together to hang out for another hour or two in their favorite bar or restaurant. For them having lots of social contact actively reduces stress (contrary to many remote believers for whom too much of it induces stress).</li><li>Constant feedback-loops are highly valued. The back and forth when discussing ideas with others brings out the best in many Officionados.</li></ul><p>Obviously those lists are neither exhaustive nor exclusive and not everyone in every group matches all of them. But the patterns are quite obvious — at least in our team.</p><p>Now, for me the most interesting insight unfolds when you look at what Officionados love about working from home from time to time and what Remoters enjoy during their visits at the office.</p><p>Here is a secret: <em>Most Remoters actually like meeting their team mates </em>— yes, they do! For them (well, I should say “us”) it is comparable to a birthday. You are looking forward to it, but it would wear off quickly if you had birthday everyday. Heck, birthdays even wear off for most people anyway over the years even if they are just once a year. Similarly Remoters don’t need in-person meetings and water cooler talks to work productively. And they don’t need it daily because it also induces a certain level of stress — commuting to the office, meeting new people in shared offices, different coffee or tea (!) and not being able to go offline if it becomes too much and focus becomes impossible.</p><p>So for Remoters visiting the office space and seeing their team in person is something special and unique — a highlight, not the default.</p><p>And Officionados? Being social beings means many of them love meeting new people. And where can you meet people better than while traveling? <em>Working remotely from co-working spaces, airports and Airbnbs in other countries makes visiting new places so much easier without spending half of your vacation days on traveling to your desired destination.</em></p><p>And of course many Officionados also have families and non-work obligations and therefore, from time to time (or daily, e.g. during the first half of the day), <em>love the unprecedented freedom of working remotely to get everything done</em>.</p><p>It turns out, most of our team members are actually both: Officionados and Remoters. It’s just the ratios which differ — though quite substantially for some. And as life changes, so do preferences and needs.</p><p>So maybe, it’s not about Remoters vs Officionados. It’s about finding the right way to work for you at this very moment in your life. And supporting others in doing the same.</p><p>And that’s what we are trying to do over here at whisp. Yes, I’m fully aware that this <em>remocal</em> setup is certainly bound to spark a lot more interesting (and at times heated) discussions in the future. 😅</p><p><strong>But in the end for a company the most important thing is that everyone working there can do so at their very best.</strong> And this is only possible if we can create a work environment that works for everyone. For some this is a beautiful and lively office space. For others it’s a cozy home office. And for many it’s both and is as fluid as life itself.</p><p><em>*I admit this post turned out different than I initially intended it to be when I started to write and I’ve actively decided against going over it once more and redacting it (except for adding that little question mark in the intro). As reflection can result in the most curious insights. Most curious indeed… </em>🤔 <em>And sometimes it can help to have the meandering river of thought written down rather than redacting it away. As it is very likely that at some point in your life you will be back at the source of river, caught in the same state of mind. And reading written down your thoughts from years ago when you were in the same situation can provide a nice shortcut to the delta and the open sea of possibilities and freedom of mind.</em></p><p><em>I will surely read these lines again in the future. Here’s a heads-up to my future self:</em></p><blockquote>Panta rhei — everything flows. (Simplikios)</blockquote><p><em>So never say never, you stubborn donkey.</em></p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a45ef8e5ae97" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A lesson in humility]]></title>
            <link>https://ubuntudroid.medium.com/a-lesson-in-humility-c338880b12f1?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/c338880b12f1</guid>
            <category><![CDATA[kotlin-native]]></category>
            <category><![CDATA[humility]]></category>
            <category><![CDATA[lessons-learned]]></category>
            <category><![CDATA[sdk]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sat, 17 Sep 2022 17:16:56 GMT</pubDate>
            <atom:updated>2022-12-10T10:41:20.955Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t3p8I0003WwPi_bU" /><figcaption>Photo by <a href="https://unsplash.com/es/@rojekilian?utm_source=medium&amp;utm_medium=referral">Sarah Kilian</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><em>It happened again.</em></p><p><em>I grossly underestimated integrating a third party SDK into our iOS app based on Kotlin/native. And by grossly I mean not just 2x or 5x, but an order of magnitude. I’m not proud of it, but it’s just what it is and it’s certainly worth a blog post as I learned one or two (actually 4, feel free to skip to the last section if you are in a hurry) things from it — and I guess I just need to write this down to finally start looking at it not just as a gigantic waste of time, but also as a lesson in patience and humility.</em></p><p>First of all, technically I lied in the intro: I underestimated the effort by *at least* an order of magnitude, as I am still not done yet. After two weeks. Not working straight on it, but on and off, which is kind of worse as it really created a lot of context switching overhead. That being said, let’s start from the beginning.</p><h3>It was just another task on just another day…</h3><p><a href="https://onesignal.com">OneSignal</a> is a push and in-app messaging SDK for mobile apps. It helps facilitating communication with your user base and as such is quite important for many mobile apps.</p><p>OneSignal integration into our (not Kotlin/native based, but just plain Kotlin) Android app was easy peasy. Just as expected I was done in a day including extensive testing.</p><p>So I expected around two days for iOS integration, as firstly, I have nowhere as much iOS experience as Android experience and secondly, our iOS app is based on Kotlin/native which potentially could complicate things. However, I didn’t expect much trouble from the latter as the OneSignal SDK was supposed to just touch the Swift side of things, so what could go wrong?</p><p>Well, turns out, hell of a lot!</p><p>The OneSignal SDK does something clever when you integrate it into your app (no matter whether you are using Swift Package Manager or Cocoapods). Without writing a single line of code its existence as a dependency alone will trigger a process called <em>swizzling</em> on app start. I am not super proficient in iOS development — I am more on a _can-make-things-work_ level, but here is what I understood swizzling does at a very high level: at app start (so at runtime, not at compile time) swizzling will exchange/add implementations of functions. This is usually done so that developers don’t need to implement certain calls to the SDK on their own and in the process make mistakes by calling the wrong functions at the wrong time.</p><p>In OneSignal’s case they are swizzling push notification handling capabilities which should make my life as an engineer easier. <em>Should</em>. The problem is, the way they are doing swizzling changed initialization/loading order of certain classes — something the Kotlin/native code at the other end of the food chain didn’t expect!</p><h3>What the swizzle?</h3><p>My first thought was: why does the Kotlin/native code even bother? It’s all Swift stuff being touched by OneSignal, or is it? Turns out, it’s not. Swizzling causes virtually all classes in your binary to be touched — that’s just how it works from what I understood from the more advanced documentation and posts you can find online. It seems to follow the visitor pattern: visit all classes and functions and then ask whether the implementation should be changed. If all classes are touched, so are the Kotlin/native classes.</p><p>And that’s the culprit. Right at app start, the app crashed with a SIGABRT (feel free to ignore the trace, it’s just here for the show effect):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/030ae89c9b8ad666777b035828358bf8/href">https://medium.com/media/030ae89c9b8ad666777b035828358bf8/href</a></iframe><p>Yes, that’s what I, with my limited iOS knowledge, got to see after launching my app. Quite of a mouthful, huh? There are better ways to start your day, believe me.</p><p>It took me some time to figure out what was happening — although that’s actually an exaggeration. I didn’t really know what was going on, just that swizzling somehow seemed to interfere with some Kotlin/native magic.</p><h3>Road to redemption?</h3><p>So I did what every responsible engineer does: fire up Google and search for what I thought was causing the problem. In lieu of meaningful search results (Kotlin/native still isn’t as widely employed as it could potentially be) I then reached out to the OneSignal and Kotlin/native developers.</p><p>If anyone is interested, here are the bug reports:</p><p><a href="https://github.com/OneSignal/OneSignal-iOS-SDK/issues/1042">Crash at app start with Kotlin/native; objc_getClassList causing initialize to fire before load on non-OneSignal · Issue #1042 · OneSignal/OneSignal-iOS-SDK</a></p><p>and</p><p><a href="https://youtrack.jetbrains.com/issue/KT-50982#focus=Comments-27-5741956.0-0">https://youtrack.jetbrains.com/issue/KT-50982#focus=Comments-27-5741956.0-0</a></p><p>Luckily, developers of both OneSignal and Kotlin turned out to be very responsive, kudos! And the Kotlin engineer I talked to even came up with a quick to implement workaround, after I had managed to create a sample project for him to replicate the issue: I had used CocoaPods to integrate the OneSignal SDK, he asked me to use SwiftPM instead and at the same time make the Kotlin/native shared code module a dynamic framework.</p><p>And it worked! As you might expect I was overjoyed! And it got even better, the Kotlin native engineer already had implemented a fix for the issue which is scheduled to be released in about 3 months as part of the next Kotlin beta.</p><p>Actually, I would have preferred to stay with CocoaPods and the static framework and instead disable swizzling for the OneSignal SDK, but it’s not clear yet, whether the latter is possible — the cost of taking away much of the integration work from user-engineers is usually paid by the same engineers when their setup is not as expected. ¯\_(ツ)_/¯</p><p>At that point, I had spent about 2 weeks on the problem. Phew.</p><p>Time to merge. I created the PR and our CI started to do it’s thing. Part of the _thing_ is to create and export an archive for internal testing purposes. And guess what? Yup, exporting failed.</p><p>shared not found in dylib search path</p><p>Bummer.</p><p>By changing the shared Kotlin/native module to be a dynamic instead of a static framework I screwed up linking big time! At the time of writing this issue has not yet been solved despite two days of try and error (once more violating learning #3, see below). I’ll update the post when we have a solution for that problem.</p><h4>Update</h4><p>There finally is a solution, and it consists of fixes introduced to the latest versions of Kotlin and OneSignal. Big thanks to the amazing teams over there for going the extra mile here and fixing this awkward issue! 👏</p><h3>Lessons learned</h3><ol><li><strong>Never underestimate the integration costs of a third party SDK</strong>, especially if it promises to be integrated “in less than 10 lines of code” (quoting the OneSignal landing page).</li><li><strong>Never underestimate the added costs when employing a growing, but not yet established technology</strong> (such as Kotlin/native in my case). However, to be fair, this was the first time I stumbled on such an issue with Kotlin/native, never had any similar problems over 2 years of development, so it also seems to be a special case.</li><li><strong>Never overestimate your own skills and instead ask for help, when you need it.</strong> It took me three days until I reached out to the OneSignal and Kotlin engineers. Before I spent way to much time trying to debug things I clearly had no real idea of. That doesn’t mean one should bombard dev-support or StackOverflow without spending some time to think about a solution on your own. But you should foster the mental clarity to see when you need help. And when you reach out to your fellow developers: ensure that your bug report adheres to their project’s guidelines and provide all the information needed in a concise and complete manner. It’s just a matter of etiquette and respect of their time.</li><li><strong>Never underestimate the time it takes to create a sample project. </strong>Here, it took me over two days to finally boil it all down to the essentials and making sure that the issue is still reproducible.</li></ol><p><em>Now, any of those learnings should be new to someone who spent more than two decades on coding (oy, I’m getting old…). But it helps to reiterate them from time to time to overcome the hubris that sometimes comes with growing experience.</em></p><p>I will probably end up to be the person reading this post the most — it shall serve as a reminder for my future self, but might also help you, dearest reader, to avoid the estimation traps I fell into.</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c338880b12f1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Labeling GitHub auto-merge pull requests]]></title>
            <link>https://ubuntudroid.medium.com/labeling-github-auto-merge-pull-requests-d5a86f62280a?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/d5a86f62280a</guid>
            <category><![CDATA[github]]></category>
            <category><![CDATA[pull-request]]></category>
            <category><![CDATA[github-auto-merge]]></category>
            <category><![CDATA[github-actions]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sat, 17 Sep 2022 17:02:04 GMT</pubDate>
            <atom:updated>2022-12-10T10:42:59.339Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Tn8tbbFsjDpQiQD2" /><figcaption>Photo by <a href="https://unsplash.com/@synkevych?utm_source=medium&amp;utm_medium=referral">Roman Synkevych 🇺🇦</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>It’s that time of the year where many of us revise their workflows and try to optimise. And boy, there is always quite a lot to improve!</p><p>This time my GitHub workflow was ripe for a productivity check. Something, that I always found cumbersome since the inception of the amazing GitHub auto-merge feature was, that you couldn’t see whether a pull request was due for auto-merge or not in the pull request overview page:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zUeXgTKUBRG6hdP0jpdVzQ.png" /><figcaption>GitHub pull request overview</figcaption></figure><p>For everyone who doesn’t know auto-merge: it essentially allows you to tell GitHub to merge a pull request as soon as all checks are green. Usually this means after the CI has successfully build the branch and all checks have passed. No more waiting for that little checkmark to turn green to be able to press the merge button! ✅</p><p>Even when working alone on a repository there can be quite a lot of (automated) pull requests with tools like <a href="https://github.com/dependabot">Dependabot</a> and the likes.</p><p>When it’s review and merging time, for me that usually means lots of unnecessary clicking on yellow state (that is in progress of being checked) pull requests which actually were already queued up for auto-merging — thanks for nothing, bad short-term memory! 🤯</p><p>A simple way to improve this is labeling pull requests for which auto-merge had been enabled previously which makes those PRs easy to spot and simple to filter in the pull request overview.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BFFBcWNMZNhqr0BDqTpOWA.png" /><figcaption>GitHub pull request overview with auto-merge labels</figcaption></figure><p>Turns out, that’s super easy to do with GitHub actions! And to make it even simpler for you, I’ve packaged this into an action ready to be used via the Github marketplace:</p><p><a href="https://github.com/marketplace/actions/label-auto-merge-prs">Label auto-merge PRs - GitHub Marketplace</a></p><p>You can integrate this into your repository with a straightforward 2-step process:</p><ol><li>Create a new workflow file in your repository, usually under .github/workflows. You can give it any name, e.g. automerge-labeler.yml.</li><li>The content of the file should be something like the following:</li></ol><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ea0269048bed5dc9483114d68c9d3178/href">https://medium.com/media/ea0269048bed5dc9483114d68c9d3178/href</a></iframe><p>What this configuration file instructs the workflow to do is:</p><ol><li>Monitor all pull requests for enable or disable auto-merge events.</li><li>Instruct the automerge-labeler action to (un-)label those pull requests with auto-merge.</li></ol><p>Commit and push and boom! 💥 All auto-merge pull-requests will now automatically be labeled!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/505/1*xyRbz8PJR8P7vpgAjuE0vw.png" /><figcaption>Our action bot has added the auto-merge label after we have enabled auto-merge</figcaption></figure><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d5a86f62280a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CoordinatorLayout Behaviors- Change elevation of a View upon scrolling another one]]></title>
            <link>https://ubuntudroid.medium.com/coordinatorlayout-behaviors-change-elevation-of-a-view-upon-scrolling-another-one-49f4711d3e67?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/49f4711d3e67</guid>
            <category><![CDATA[coordinatorlayout]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sun, 23 Jun 2019 09:12:40 GMT</pubDate>
            <atom:updated>2022-12-10T10:43:31.210Z</atom:updated>
            <content:encoded><![CDATA[<h3>CoordinatorLayout Behaviors: Change elevation of a View upon scrolling another one</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*G6W4rgI00e_euSe4" /><figcaption>Photo by <a href="https://unsplash.com/@sxoxm?utm_source=medium&amp;utm_medium=referral">Sven Mieke</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Consider this: You need a button to change its elevation depending on the scroll state of a RecyclerView (in fact any ScrollingView): the button (which for various reasons I decided to implement as an ImageView should have zero elevation when the RecyclerView is at scroll position 0 and 6 dp elevation otherwise.</p><p>The way to make this work is by leveraging CoordinatorLayout and a custom Behavior.</p><p>Behaviors are a quite flexible CoordinatorLayout feature. You can attach behaviors to any direct child of a CoordinatorLayout (or children of ViewGroups which define their own default behavior, but I won&#39;t dive into this here).</p><p>You can set behaviors either in code or in XML. I did the latter:</p><pre>&lt;androidx.coordinatorlayout.widget.CoordinatorLayout<br>   ...&gt;<br>    <br>   &lt;ImageView<br>      ...<br>      app:layout_anchor=&quot;@id/rcv&quot;<br>      app:layout_anchorGravity=&quot;top|start&quot;<br>      app:layout_behavior=&quot;com.example.ElevateOnScrollBehavior&quot;<br>      android:elevation=&quot;@{0}&quot; /&gt;<br>   <br>   &lt;RecyclerView<br>      ...<br>      android:id=&quot;@+id/rcv&quot; /&gt;<br> &lt;/androidx.coordinatorlayout.widget.CoordinatorLayout&gt;</pre><p>First we are setting the initial elevation to 0 using android:elevation, you&#39;ll note that this is a data binding expression here - more on that at the end of this post.</p><p>app:layout_behavior=&quot;com.example.ElevateOnScrollBehavior&quot; defines our elevation modifying behavior.</p><p>app:layout_anchor=&quot;@id/rcv&quot; tells the behavior which target view is relevant for it and should report its scroll state.</p><p>app:layout_anchorGravity=&quot;top|start&quot; defines the position of the ImageView relative to the RecyclerView which for that sake is handled like a FrameLayout, that&#39;s why we can just define a gravity and need to fake everything else using paddings and margins.</p><p>Let’s have a look at the behavior. For our use case we are interested into to functions in the CoordinatorLayout.Behavior interface: onStartNestedScroll() and onNestedScroll()</p><p>The first is called <em>before</em> the actual scroll happens and allows us to decide whether we are interested in it based on properties like the axis or the scroll target.</p><p>The latter is then called every time a subsequent scroll happens and this is where we employ our magic.</p><pre>class ElevateOnScrollBehavior&lt;V : View&gt; @JvmOverloads constructor(<br>    context: Context,<br>    attrs: AttributeSet? = null<br>) : CoordinatorLayout.Behavior&lt;V&gt;(context, attrs) {<br><br>    override fun onStartNestedScroll(<br>        coordinatorLayout: CoordinatorLayout,<br>        child: V,<br>        directTargetChild: View,<br>        target: View,<br>        axes: Int,<br>        type: Int<br>    ): Boolean =<br>        axes == ViewCompat.SCROLL_AXIS_VERTICAL &amp;&amp;<br>                target is ScrollingView<br><br>    override fun onNestedScroll(<br>        coordinatorLayout: CoordinatorLayout,<br>        child: V,<br>        target: View,<br>        dxConsumed: Int,<br>        dyConsumed: Int,<br>        dxUnconsumed: Int,<br>        dyUnconsumed: Int,<br>        type: Int,<br>        consumed: IntArray<br>    ) {<br>        if ((target as ScrollingView).computeVerticalScrollOffset() &gt; 0) {<br>            6.dpToPx(coordinatorLayout.context.resources.displayMetrics).scoped { newElevation -&gt;<br>                if (newElevation != child.elevation) {<br>                    child.elevation = newElevation<br>                }<br>            }<br>        } else if (child.elevation != 0f) {<br>            child.setZeroElevation()<br>        }<br>    }<br>}</pre><p>As you can see we are only interested in scrolls along the vertical axis and only in those coming from ScrollingViews as this interface has a method we are going to use in onNestedScroll() to determine the vertical scroll offset of the target view. Conveniently RecyclerView implements ScrollingView, so we are good to go here.</p><p>In onNestedScroll() we simply check for the vertical scroll offset and if it is bigger than zero we give the View some (fixed for now) elevation, otherwise we set it to zero.</p><p>Unfortunately using View.setElevation(0f) would result in a non-clickable view for some reason. Therefore I am using my custom View.setZeroElevation() extension function and a custom data binding adapter for android:elevation. More on that in <a href="https://medium.com/@ubuntudroid/zero-elevation-zero-click-handling-271a08489071">Zero elevation - zero click handling?</a>.</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49f4711d3e67" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Zero elevation — zero click handling]]></title>
            <link>https://ubuntudroid.medium.com/zero-elevation-zero-click-handling-271a08489071?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/271a08489071</guid>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sun, 16 Jun 2019 09:30:12 GMT</pubDate>
            <atom:updated>2022-12-10T10:43:58.352Z</atom:updated>
            <content:encoded><![CDATA[<h3>Zero elevation — zero click handling?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4_fmgvCczuN9iq3H" /><figcaption>Photo by <a href="https://unsplash.com/@jfielitz?utm_source=medium&amp;utm_medium=referral">Jeff Fielitz</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><em>Something I’ve only discovered after 10 years of Android development: Setting zero elevation on clickable elements causes them to not being clickable anymore!</em></p><p>I haven’t yet investigated whether this is a bug or a feature, but here is a simple workaround (by means of BindingAdapters, but of course you can also use the supplied View-Extension function on its own):</p><pre>@BindingAdapter(&quot;elevation&quot;)<br>fun setAppElevation(view: View, elevation: Float) {<br>    setElevation(view, elevation)<br>}<br><br>@BindingAdapter(&quot;android:elevation&quot;)<br>fun setAndroidElevation(view: View, elevation: Float) {<br>    setElevation(view, elevation)<br>}<br><br>private fun setElevation(view: View, elevation: Float) {<br>    if (elevation == 0f) {<br>        view.setZeroElevation()<br>    } else {<br>        view.elevation = elevation<br>    }<br>}<br><br>fun View.setZeroElevation() {<br>    elevation = 0f<br>    bringToFront() // this fixes 0 elevation views not being clickable<br>}</pre><p>The trick is to call bringToFront() after setting the elevation to 0dp - this makes the View recognize click events again. By using BindingAdapters and forwarding to View.elevation we ensure that we don&#39;t break existing elevation functionality for Views.</p><p>You can simply use this in your layout XML like this:</p><pre>&lt;View<br>      ...<br>      app:elevation=&quot;@{0}&quot; /&gt;</pre><p>or</p><pre>&lt;View<br>      ...<br>      android:elevation=&quot;@{@dimen/zero}&quot; /&gt;</pre><p>where @dimen/zero is just a fictional 0dp dimension resource which is automatically converted to float pixels by data binding, but obviously it could be any expression resulting in a float.</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=271a08489071" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Bring some zen to your shell]]></title>
            <link>https://ubuntudroid.medium.com/bring-some-zen-to-your-shell-5d41c12e1627?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/5d41c12e1627</guid>
            <category><![CDATA[shell]]></category>
            <category><![CDATA[zen]]></category>
            <category><![CDATA[terminal]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Tue, 09 Apr 2019 15:49:45 GMT</pubDate>
            <atom:updated>2022-12-10T10:44:19.298Z</atom:updated>
            <content:encoded><![CDATA[<p>Sifting through the web I stumbled upon a little API called <em>zen</em> which is provided by the guys over at GitHub. The API returns a random (mostly developer focused) piece of wisdom and is accessible via <a href="https://api.github.com/zen.">https://api.github.com/zen</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*azuxFAuUGN6Sf81vueSOug.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/qG-hPxoAnRE">Johnson Wang via Unsplash</a></figcaption></figure><p>I thought it might be nice to have such a thing every time I open my shell (I am using <em>iterm2</em> with <em>zsh</em> for what it’s worth, but the following will work with nearly every shell out there).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/703/1*agxRQEUK2eeGoyGGSk17RQ.png" /><figcaption>Sound advice…</figcaption></figure><p>To make it happen first locate your shell’s configuration file. Usually you can find those in your home folder, oftentimes they start with a dot (.) and end with rc. In the case of zsh the file is called .zshrc. In case of bash (the default shell on many systems) it is .bashrc. If the file doesn’t exist yet simply create it in your home folder.</p><p><strong>TL;DR:</strong> To get exactly the output like in the screenshot above you would append the following two lines to your rc file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e2acf7d8d4e2012f494b637d0b074d2e/href">https://medium.com/media/e2acf7d8d4e2012f494b637d0b074d2e/href</a></iframe><p>And here is what we did:</p><p>To output something at every start of the shell just add a line at the bottom of the configuration file. E.g. to print a simple static text we would append:</p><p>echo &quot;Welcome, commander!&quot;</p><p>To print a quote from the zen API we need to use curl instead which is a pretty common way of performing network requests via the shell:</p><p>curl -s -m 1 https://api.github.com/zen</p><p>-s is short for --silent and does just that: it won’t show any errors or timeouts or request-meta information like status codes, just the plain response.</p><p>-m 1 controls the timeout: In our case we want it to be rather small as usually you fire up the terminal quite often and want it to be available as fast as possible. So this sets the timeout to 1 second.</p><p>Finally -w &#39;\n&#39; takes care of adding a newline character at the end of the curl response. Otherwise some shells (like zsh) would print out a percent sign at the end of the line.</p><p>And that’s it. Enjoy the zen. ☯️</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5d41c12e1627" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Missing signing config when debugging release builds]]></title>
            <link>https://ubuntudroid.medium.com/missing-signing-config-when-debugging-release-builds-9a49236157fe?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/9a49236157fe</guid>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Sun, 13 May 2018 09:38:58 GMT</pubDate>
            <atom:updated>2022-12-10T10:44:39.347Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to fix missing signing configuration when locally debugging Android release builds</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j0Mmygnj3bYeedHIJH_Wqg.jpeg" /></figure><p>Oftentimes, when locally building release builds, e.g. to debug issues, you don’t have access to the signing config as you maybe just have it available on your CI — after all you should really try to avoid storing your release keystores on your local development machine.</p><p>However, Android Studio will complain (inside the run configuration screen) about a missing signing configuration if you try to build release variants without:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rw0fAbUZxpaQ7M-izbYwag.png" /></figure><p>In that case you can simply use the debug keystore by adding the following line to your release build type definition in your app’s <em>build.gradle</em>:</p><pre>android {<br>  buildTypes {<br>    release {<br>      signingConfig debug.signingConfig<br>    }<br>  }<br>}</pre><p>That should allow you to compile and run the release version of your app locally.</p><p>Please note that in app purchases won’t work with the debug certificate. You also won’t be able to upload an APK signed with the debug certificate to the Play Store.</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9a49236157fe" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using String placeholders with data binding]]></title>
            <link>https://ubuntudroid.medium.com/using-string-placeholders-with-data-binding-394615354464?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/394615354464</guid>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[android-data-binding]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Fri, 27 Apr 2018 15:00:05 GMT</pubDate>
            <atom:updated>2022-12-10T10:45:15.828Z</atom:updated>
            <content:encoded><![CDATA[<h3>Using String resource placeholders with data binding</h3><p>Today I stumbled about one of these tiny things which — once you uncover them — immensely ease your life.</p><p>Consider a String with placeholders stored in your <em>strings.xml</em>:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7aa1cc42be242742a9b9079648435fb4/href">https://medium.com/media/7aa1cc42be242742a9b9079648435fb4/href</a></iframe><p>Now let&#39;s say we want to use that String directly in data binding. Before figuring out the following trick I would fetch the data from the attached ViewModel which would use String.format() to replace the placeholder with e.g. the user name.</p><p>Turns out there is a by far easier way to do this directly within the layout:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/58d629f72d25cc8c1e7506a173c65077/href">https://medium.com/media/58d629f72d25cc8c1e7506a173c65077/href</a></iframe><p>Isn&#39;t that neat?</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=394615354464" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to access a list of resources via data binding]]></title>
            <link>https://ubuntudroid.medium.com/how-to-access-a-list-of-resources-via-data-binding-6f38a3786499?source=rss-4fcaefd1f0f9------2</link>
            <guid isPermaLink="false">https://medium.com/p/6f38a3786499</guid>
            <category><![CDATA[ui]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[android-data-binding]]></category>
            <dc:creator><![CDATA[Sven Bendel]]></dc:creator>
            <pubDate>Fri, 13 Apr 2018 17:13:57 GMT</pubDate>
            <atom:updated>2022-12-10T10:45:41.268Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to access a list of resources via data binding (and create nice gradient backgrounds along the road)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TbijcAPnUmnVwEdMJe2ZnA.png" /></figure><p>Let’s imagine you need to provide a gradient background consisting of arbitrarily many colors to a View.</p><p>This can’t be done via regular drawable XML as a gradient within a shape can only define three colors: startColor, centerColor, endColor.</p><p>To reach our goal we will be using data binding.</p><p>We define an integer array in e.g. <em>arrays.xml</em> and fill it with the desired color resources:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2771c4ab73de62be5880162fb132c274/href">https://medium.com/media/2771c4ab73de62be5880162fb132c274/href</a></iframe><p>Then we define a binding adapter to apply the colors in a gradient to the background of the View:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ee4117f63728636d840fa0bf594c1e34/href">https://medium.com/media/ee4117f63728636d840fa0bf594c1e34/href</a></iframe><p>Now we can define the background colors in our layout XML (apply it to any View class you like):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7750e852daadcd070296681824723cb3/href">https://medium.com/media/7750e852daadcd070296681824723cb3/href</a></iframe><p>Note, that we have to reference integer-array resources with @intArray in data binding expressions!</p><figure><a href="https://www.buymeacoffee.com/ubuntudroid"><img alt="" src="https://cdn-images-1.medium.com/max/170/1*9vg3-OY14aZN1UpKwIxxZg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6f38a3786499" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>