Check out my apps: miaocast and Celluloid.

Building Lynx: Day Two

6 min read

Second day building Lynx, a new mastodon client, and I’ve made some pretty good progress: I can now login to a mastodon server and fetch the home timeline.

Here is what the app looks like at the end of day two:

Login and home page

Double checking AI’s home work

So far I have not really incorporated AI into my daily coding task beside the occasional trip to ChatGPT when I wasn’t feeling like digging through links in Google search. Usually I get mixed results from those kind of tools so I haven’t really been spending time to really dig into them.

Today, I did try out Gemini in Android Studio beyond the autocomplete suggestion feature I’ve opted in for a while. The task at hand was to generate data classes based on the mastodon API documentation. For example, generating the Status entity class based on this web page. Genmini spit out some boilerplate and was mostly correct but it made up some stuff that are not present in the documentation page.

In the end, I had to go over the generated code to double check that it is actually correct. Overall estimate is that it may have saved me 10 to 15 minutes, so I’m still not convinced by the hype that those tools are pushing.

Time Sinks

Other than the tedious task of supervising AI to generate the REST API boilerplate code, I also ran into a few of your standard day to day issues that are part of the developer life.

It’s worth to be reminded of two things:

  1. Always read the error messages carefully and then read it again before trying a fix.
  2. It’s super important to write good error message, your future self will thank the present you.

A case of missing dependency

I’ve been a very happy coil user in miaocast so it was quite strange that loading image in the new app is not working. After adding some logging like the following code:

val state by painter.state.collectAsStateWithLifecycle()
when (val painterState = state) {
    is AsyncImagePainter.State.Error -> {
        LaunchedEffect(painterState) {
            Timber.tag("NetworkImage")
                .e(painterState.result.throwable, "Failed to load image %s", src)
        }
        // ...
    }

    else -> {}
}

I got a pretty strange error:

Unable to create a fetcher that supports https://…

Turns out, as part of the move to make coil multiplatform, it no longer comes with a network stack by default. To load network image, one has to add a new dependency. This is mostly my fault for not reading the doc, where this is clearly stated but it would be nice for coil to log something to the console by default to warn about this.

A case of mismatched redirect uri

Another big time sink today was to get mastodon’s oauth flow working. At first, I missed the part where after you get the user to grant the app permission, what you get is not the final access_token. Instead, the app has to exchange this code for an access_token with another api call.

Once I figured that out, I run into another error where mastodon is saying the code I used to exchange for access_token is invalid. Turns out, you must use a redirect_uri that is specified when you created the app with the server and I was using the wrong value for this parameter.

In the app, I use the deeplink lynx://oauth-callback/ as the base URL for oauth callback.

<intent-filter>
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:host="oauth-callback"
        android:scheme="lynx" />
</intent-filter>>

At runtime, I dynamically construct the redirect URL based on which server I’m talking to, so that when the callback is made, the app can tell which server the callback is for.

// For mastodon.social, the oauth redirect uri will be
// lynx://oauth-callback/mastodon.social
fun makeRedirectUri(server: String): String =
      "${AppConfiguration.OAUTH_REDIRECT_URI}/$server"

I was setting the redirect_uri to AppConfiguration.OAUTH_REDIRECT_URI when I should have set it to makeRedirectUri(server) instead. Again, this is clearly stated in the error response (kudos to mastodon) and it just took me a while to figure it out.

A case of missing alpha

One last issue I had was with the haze library that I use to add a blurred background to the top bar. Somehow, I forgot that you are supposed to use a semi-transparent color for the tint and struggled with this for a good 10 or 15 minutes until I figured out what the issue is.

What’s Next

My main goal tomorrow is to make progress towards rendering the statuses in the timeline view, specifically:

  • better date formatting
  • support custom emoji in the display name
  • displaying the HTML correctly
  • support displaying image media attachment
  • start thinking about the overall look and feel of the app

My hope is that I can reuse most of the work I did from previous attempt and spend more time in developing a visual style for the app.


Xiaoming's Blog

Indie developer based in San Jose, California.