MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Orbital Insertion Successful: Space Junkies Uganda Goes Live

2026-03-02 11:42:59

This is a submission for the DEV Weekend Challenge: Community

"Uganda may not have launched a rocket yet, but we've launched something just as important: interest." 🚀

The Community

Space Junkies Uganda (SJU) was born out of deep love for space, for Uganda, and for the belief that the cosmos belongs to everyone, including us.

I'm Ronnie Atuhaire, and I've been procrastinating on building our web presence since we started. But this challenge? This was the push I needed.

The Origin Story:

Last year, I was approached to serve as a judge for the NASA Space Apps Challenge in Kampala. During that event, I witnessed something powerful: students weren't just interested in space, they were passionate about it. Their eyes lit up when talking about black holes, Mars missions, and Uganda's own satellite, PearlAfricaSat-1 (launched in 2022!).

Around the same time, I became the temporary National Coordinator for World Space Week in Uganda. If you're into space, you know October is the month. So we went all in.

Uganda Space Week 2025 (October 8-10) was our first national celebration of space and astronomy, held at Makerere University. Over three days:

  • 🎯 55+ attendees (students, engineers, dreamers)
  • 🌐 2 virtual sessions + 1 physical meetup
  • 🎬 Space movie marathon (Interstellar, Gravity, Passengers)
  • 🔭 First telescope session (I pointed it the wrong way at first, but hey, we still made contact with the stars 😂)
  • 🚀 DIY rocket demos
  • 🎲 Space Bingo Networking
  • 🧠 Topics: Quantum Cosmology, Area 51, Cryogenics, Black Holes, Living in Space

More info 👇

#ugandaastronomicalsociety #worldspaceweek #ieee #nasaspaceapps #spacejunkiesug | Ronnie Linslay Atuhaire

Uganda Space Week: What you Missed 🚀🌌 A few days ago, we were out of this world; literally. We kicked off the week with the NASA Space Apps Challenge, where I had the honor of serving as a local judge (yes, I finally got to say “your idea is going to space!” 😄). Then came Uganda Space Week (8th–10th October); our very first national celebration of space and astronomy, held at Makerere University. Over three days, we hosted two virtual sessions and a physical meetup that drew over 55 attendees; from curious students to die-hard space fans. We explored everything from Quantum Cosmology to Area 51 conspiracies, from Cryogenics, Black Holes to 𝗟𝗶𝘃𝗶𝗻𝗴 𝗶𝗻 𝗦𝗽𝗮𝗰𝗲, featuring movies like Interstellar, Gravity, and Passengers. We also had some fun; Space Bingo Networking, DIY Rocket demos, and yes, I operated a telescope for the first time (spoiler: I pointed it the wrong way at first; but hey, we still made contact with the stars 😂). During our stargazing session, I realized, this wasn’t just an event; it was a connection between imagination and science, between curiosity and community. As an IEEE Chair and space enthusiast, organizing this wasn’t just a responsibility, it was pure passion. Uganda may not have launched a rocket yet, but we’ve launched something just as important; interest. 𝘋𝘪𝘥 𝘺𝘰𝘶 𝘬𝘯𝘰𝘸 𝘜𝘨𝘢𝘯𝘥𝘢 𝘢𝘭𝘳𝘦𝘢𝘥𝘺 𝘩𝘢𝘴 𝘗𝘦𝘢𝘳𝘭𝘈𝘧𝘳𝘪𝘤𝘢𝘚𝘢𝘵-1, 𝘰𝘶𝘳 𝘧𝘪𝘳𝘴𝘵 𝘴𝘢𝘵𝘦𝘭𝘭𝘪𝘵𝘦, 𝘭𝘢𝘶𝘯𝘤𝘩𝘦𝘥 𝘪𝘯 2022? 🌍✨ Oh, and a fun fact; a NASA astronaut’s suit costs about $12 million (around UGX 46 billion). 𝘐 𝘱𝘳𝘰𝘮𝘪𝘴𝘦 𝘰𝘶𝘳 “𝘚𝘱𝘢𝘤𝘦 𝘑𝘶𝘯𝘬𝘪𝘦𝘴 𝘜𝘨𝘢𝘯𝘥𝘢” 𝘛-𝘴𝘩𝘪𝘳𝘵𝘴 𝘸𝘪𝘭𝘭 𝘣𝘦 𝘸𝘢𝘺 𝘤𝘩𝘦𝘢𝘱𝘦𝘳 😅 A huge thank you to all IEEE core members, our amazing partners, and every space enthusiast who made this journey possible. You are the real stars 🌠 If you’re passionate about space and want to join our growing community; Space Junkies Uganda, or grab some cool space merch, reach out! Join us Now: https://lnkd.in/d2ykg7Gj Here’s to making the next frontier of discovery start right here in Uganda 🇺🇬🚀 Thanks to Zoora Harrison, Obwengye Cosmus, Aerobuddies, GDG on Campus - Makerere University, Dr Byaruhanga Christopher , Anthony Ijoot, AHABWE CLARISSA PETITE, Ninsiima Patricia Banura, Angel Uwera, Ritah Ndibalekera, Iradukunda Jemimah, Shatrah Ddamulira, Asiimwe Martha Serena, #UgandaAstronomicalSociety IEEE Makerere University Student Branch, Ayebazibwe Brinton, Innocent Oluka et al who made this experience amazing 🚀🔥😍 #WorldSpaceWeek #IEEE #NASAspaceapps #SpaceJunkiesUG

favicon linkedin.com

During our stargazing session at Kololo Hill, I realized: this wasn't just an event. It was a connection between imagination and science, between curiosity and community.

Why Uganda?

We sit on the equator (0.3136°N, 32.5811°E) - one of the best places on Earth for astronomical observations. We have PearlAfricaSat-1 orbiting above us. We have passionate students, amateur astronomers, and engineers who dream big.

What we didn't have was a digital home. A place to coordinate stargazing nights, share telescope observations, organize dark sky expeditions to Lake Mburo, and prove that African space enthusiasts are already here, already building.

Space Junkies Uganda is now 135+ members strong, and this platform is our mission control.

What I Built

A full-stack community platform featuring:

For Members:

  • 🎮 Asteroid Hunter Game - Defend Kampala from asteroids! Browser-based arcade game with health system, HUD metrics, and high scores. (Because if we're going to space, we need to practice defending Earth first.)
  • 🖼️ Dynamic Gallery - Upload and view photos from Uganda Space Week 2025 and community events with a smooth lightbox slider
  • 📅 Interactive Events Calendar - Track stargazing nights, telescope workshops, and dark sky expeditions
  • 🤖 Cosmos AI Chatbot - Gemini-powered assistant with Uganda-specific space knowledge (knows about PearlAfricaSat-1!)
  • 🎬 Space Movie Library - Curated collection of space documentaries and films
  • 👥 Crew Roster - Meet the community members with astronaut avatars
  • 🌌 Skywatch - Real-time ISS tracking and celestial object visibility

For Admins:

  • 📸 Gallery Manager - Upload images that automatically replace placeholders on the main site
  • 👥 Member Request System - Review and approve new members (1,000 game points required, gotta earn your spot!)
  • 📅 Event Creator - Schedule and manage community events
  • 💾 Persistent Storage - All data saved to JSON files, survives server restarts

Try it: https://spacejunkies-production.up.railway.app/

Design Philosophy:
Cyberpunk-meets-cosmos aesthetic with neon orange (#FF4500), cyan (#00FFD1), and deep space blacks. Terminal-style fonts (Orbitron, Share Tech Mono), scanline overlays, aurora effects, and floating particles create an immersive "space command center" vibe. Think Blade Runner meets the ISS.

Demo

🚀 Live Features:

  • Navigate between pages with smooth transitions
  • Play the Asteroid Hunter game (← → to move, SPACE to fire)
  • Click gallery images to open the lightbox slider
  • Chat with Cosmos AI about Uganda's space potential
  • Browse the interactive events calendar
  • Upload images via admin panel at /admin.html

Key Interactions:

  1. Gallery uploads replace placeholders one-by-one in real-time
  2. Blog posts show "Under Development" popup (auto-fades after 5s)
  3. Game HUD displays health, score, kills, accuracy, FPS
  4. Lightbox supports keyboard navigation (← → arrows, Esc to close)
  5. Events calendar highlights event days with orange glow

Code

Here is the GitHub Repo: https://github.com/Ronlin1/space_junkies

Architecture:

space_junkies/
├── server.js              # Express server + Gemini AI proxy
├── public/
│   ├── index.html         # Single-page app (all pages in one file)
│   ├── admin.html         # Admin panel
│   ├── favicon.jpg        # Site icon
│   └── uploads/           # User-uploaded gallery images
├── gallery-data.json      # Persistent gallery storage
├── members-data.json      # Member requests storage
├── events-data.json       # Events storage
├── .env                   # GEMINI_API_KEY
└── package.json           # Dependencies

Tech Stack:

  • Backend: Node.js + Express
  • AI: Google Gemini 3.1 Pro Preview via @google/genai SDK
  • File Uploads: Multer (10MB limit, auto-creates /uploads/)
  • Storage: JSON files (no database needed for MVP)
  • Frontend: Vanilla JavaScript (no frameworks!)
  • Styling: Pure CSS with custom animations

Key Features Implementation:

  1. Gallery System:
// Server: Upload endpoint
app.post('/api/admin/gallery/upload', upload.array('images', 10), (req, res) => {
  const uploadedImages = req.files.map(file => ({
    id: Date.now() + '-' + Math.random().toString(36).substring(2, 11),
    url: `/uploads/${file.filename}`,
    caption: req.body.caption || '',
    uploadedAt: new Date().toISOString()
  }));
  galleryImages.push(...uploadedImages);
  saveData(GALLERY_FILE, galleryImages);
  res.json({ success: true, images: uploadedImages });
});

// Client: Load and display
async function loadGalleryImages(){
  const res = await fetch('/api/gallery');
  const data = await res.json();
  data.images.forEach((img, index) => {
    placeholders[index].innerHTML = `
      <img src="${img.url}" alt="${img.caption}">
      <div class="gal-overlay">
        <div class="gal-label">${img.caption}</div>
      </div>
    `;
    placeholders[index].onclick = () => openLightbox(index);
  });
}
  1. Lightbox Slider:
function openLightbox(index){
  currentLightboxIndex = index;
  updateLightboxImage();
  document.getElementById('lightbox').classList.add('active');
  document.body.style.overflow = 'hidden';
}

function changeLightboxImage(direction){
  currentLightboxIndex += direction;
  if(currentLightboxIndex < 0) currentLightboxIndex = galleryImagesData.length - 1;
  if(currentLightboxIndex >= galleryImagesData.length) currentLightboxIndex = 0;
  updateLightboxImage();
}

// Keyboard navigation
document.addEventListener('keydown', e => {
  if(!document.getElementById('lightbox').classList.contains('active')) return;
  if(e.key === 'Escape') closeLightbox();
  if(e.key === 'ArrowLeft') changeLightboxImage(-1);
  if(e.key === 'ArrowRight') changeLightboxImage(1);
});
  1. Gemini AI Integration:
const { GoogleGenAI } = require('@google/genai');

app.post('/api/chat', async (req, res) => {
  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
  const response = await ai.models.generateContent({
    model: "gemini-3.1-pro-preview",
    contents: `${SYSTEM_PROMPT}\n\nUser: ${req.body.message}`
  });
  res.json({ reply: response.text });
});
  1. Asteroid Game:
  2. Canvas-based rendering with requestAnimationFrame
  3. Collision detection using distance formula
  4. Health system (30 HP, -1 per asteroid that passes)
  5. Random collision mechanic (10% chance when near player)
  6. HUD with real-time metrics (health bar, score, accuracy, FPS)

Screenshots

Home Page

Gallery

Asteroid Game

Sky Watch

How I Built It

The Journey:

I started ideating with Claude Code Opus, exploring the concept of a space community platform for Uganda. The vision was clear: create something that felt like a NASA mission control center but accessible to everyone.

Moved to Manus to prototype the UI/UX, experimenting with the cyberpunk aesthetic-neon colors, terminal fonts, scanline effects. The design language emerged: "What if Blade Runner met the ISS?"

Finally settled with Kiro and Copilot CLI for the heavy lifting. Kiro's autonomous coding capabilities were perfect for scaffolding the entire single-page app structure, implementing the game logic, and building the admin panel. Copilot CLI accelerated the API integrations and helped debug the trickier parts (like fixing the duplicate id="events" issue that broke navigation).

Development Highlights:

  1. Single-Page Architecture - All pages in one HTML file, JavaScript handles routing. Keeps deployment simple and load times instant.

  2. No Database - JSON files for persistence. For a community of 135 members, this is perfect. Simple, version-controllable, no setup required.

  3. Gemini Integration - Switched from fetch-based API to the official @google/genai SDK. Much cleaner error handling and automatic retries.

  4. Gallery Upload Flow - The trickiest part was ensuring uploads persist AND display on the main site. Solution: Server saves to JSON + filesystem, client fetches on page load and replaces placeholders sequentially.

  5. Lightbox Slider - Built from scratch with CSS animations and keyboard support. No libraries needed. Smooth zoom-in effect using cubic-bezier(.34,1.56,.64,1) for that satisfying bounce.

  6. Game Development - Canvas API for rendering, collision detection math, health system, HUD overlay. The exit button was crucial; players needed an escape hatch without losing progress.

  7. Events Page Fix - Duplicate IDs (id="events" on both home section and events page) broke navigation. Changed home section to id="home-events", added missing CSS for calendar and event cards.

  8. Favicon - Simple but important. Added <link rel="icon" type="image/jpeg" href="/favicon.jpg"> to complete the professional look.

Challenges Overcome:

  • Port conflicts - Multiple node processes blocking port 3000. Solution: taskkill /F /IM node.exe before starting.
  • Catch-all route placement - Had app.get('*', ...) BEFORE API endpoints, intercepting all requests. Moved to end of file.
  • Multer directory creation - Ensured uploads/ folder auto-creates with fs.mkdirSync(uploadDir, { recursive: true }).
  • Lightbox image loading - Stored gallery data globally so lightbox can access all images, not just visible ones.

Technologies:

  • Node.js 24.13.1
  • Express 4.21.2
  • @google/genai 1.43.0
  • Multer 1.4.5-lts.2
  • Vanilla JavaScript (ES6+)
  • CSS3 (Grid, Flexbox, Animations)
  • HTML5 Canvas (for game)

Deployment Ready:

  • Environment variables via .env
  • Persistent storage with JSON files
  • Static file serving via Express
  • Admin panel at /admin.html
  • Health check endpoint at /health

What's Next:

  • Authentication for admin panel
  • Database migration (MongoDB/PostgreSQL)
  • Image optimization (resize, compress)
  • Member join form on main site
  • Full blog post system (currently shows "Under Development" popup)
  • WhatsApp group integration
  • Email notifications for approved members
  • Space Junkies Uganda merch store (those T-shirts I promised!)

The Real Mission:

This platform isn't just about code. It's about proving that space exploration starts with curiosity, not budgets. It's about showing that a kid in Kampala can look up at the same stars as a kid in Cape Canaveral and dream just as big.

During Uganda Space Week, I operated a telescope for the first time. I pointed it the wrong way initially, but when we finally locked onto Jupiter, seeing those Galilean moons with my own eyes, that moment changed everything. That's what this platform is for: creating those moments for others.

We may not have $12 million spacesuits, but we have something better: a community that believes the next frontier of discovery starts right here in Uganda. 🇺🇬

Built with ❤️ for Space Junkies Uganda
Founded by Ronnie Atuhaire • January 2025
We're already gone. 🚀

Special thanks to all IEEE core members, our amazing partners, and every space enthusiast who made Uganda Space Week possible. You are the real stars. 🌠

Firebase Remote Config Complete Guide — A/B Testing/Feature Flags/Compose Integration

2026-03-02 11:41:02

What You'll Learn

This article explains Firebase Remote Config (real-time updates, feature flags, A/B testing, Compose integration).

Setup

// build.gradle.kts
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    implementation("com.google.firebase:firebase-config-ktx")
}

Setting Default Values

<!-- res/xml/remote_config_defaults.xml -->
<?xml version="1.0" encoding="utf-8"?>
<defaultsMap>
    <entry>
        <key>feature_new_ui</key>
        <value>false</value>
    </entry>
    <entry>
        <key>welcome_message</key>
        <value>Welcome!</value>
    </entry>
    <entry>
        <key>max_items</key>
        <value>20</value>
    </entry>
</defaultsMap>

Repository Implementation

class RemoteConfigRepository @Inject constructor() {
    private val remoteConfig = Firebase.remoteConfig.apply {
        setConfigSettingsAsync(remoteConfigSettings {
            minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
        })
        setDefaultsAsync(R.xml.remote_config_defaults)
    }

    private val _configUpdates = MutableSharedFlow<Unit>(replay = 1)

    init {
        remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
            override fun onUpdate(configUpdate: ConfigUpdate) {
                remoteConfig.activate().addOnCompleteListener {
                    _configUpdates.tryEmit(Unit)
                }
            }
            override fun onError(error: FirebaseRemoteConfigException) {
                Log.e("RemoteConfig", "Update error", error)
            }
        })
        fetchAndActivate()
    }

    private fun fetchAndActivate() {
        remoteConfig.fetchAndActivate()
    }

    fun getBoolean(key: String): Boolean = remoteConfig.getBoolean(key)
    fun getString(key: String): String = remoteConfig.getString(key)
    fun getLong(key: String): Long = remoteConfig.getLong(key)

    val updates: SharedFlow<Unit> = _configUpdates
}

Feature Flags

@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteConfig: RemoteConfigRepository
) : ViewModel() {

    val isNewUiEnabled: StateFlow<Boolean> = remoteConfig.updates
        .map { remoteConfig.getBoolean("feature_new_ui") }
        .stateIn(viewModelScope, SharingStarted.Eagerly, remoteConfig.getBoolean("feature_new_ui"))

    val welcomeMessage: StateFlow<String> = remoteConfig.updates
        .map { remoteConfig.getString("welcome_message") }
        .stateIn(viewModelScope, SharingStarted.Eagerly, remoteConfig.getString("welcome_message"))
}

Compose Screen

@Composable
fun HomeScreen(viewModel: MainViewModel = hiltViewModel()) {
    val isNewUi by viewModel.isNewUiEnabled.collectAsStateWithLifecycle()
    val welcomeMessage by viewModel.welcomeMessage.collectAsStateWithLifecycle()

    Column(Modifier.padding(16.dp)) {
        Text(welcomeMessage, style = MaterialTheme.typography.headlineMedium)
        Spacer(Modifier.height(16.dp))

        if (isNewUi) {
            NewFeatureCard()
        } else {
            LegacyContent()
        }
    }
}

@Composable
fun NewFeatureCard() {
    Card(Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp)) {
            Text("New Feature", style = MaterialTheme.typography.titleMedium)
            Text("This new UI is enabled via Remote Config")
        }
    }
}

Summary

Feature Implementation
Initialization Firebase.remoteConfig
Default values setDefaultsAsync()
Real-time addOnConfigUpdateListener
Feature flags getBoolean("key")
A/B testing Firebase Console configuration
  • Use fetchAndActivate() to get latest values
  • ConfigUpdateListener reflects changes in real-time
  • Default values XML works offline
  • Set up A/B testing in Firebase Console

8 production-ready Android app templates (Firebase integrated) are available.

Browse templatesGumroad

Related articles:

  • Firebase Auth
  • Firebase Firestore
  • Firebase Messaging

Ready-Made Android App Templates

8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.

Browse templatesGumroad

Retrofit and OkHttp Guide for Android - API Definition and Interceptors (v2)

2026-03-02 11:40:30

Retrofit and OkHttp power network operations in Android apps. Master API definitions, custom interceptors, and token-based authentication.

Defining Retrofit API Interface

Create type-safe API endpoints:

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): User

    @POST("posts")
    suspend fun createPost(@Body post: PostRequest): PostResponse

    @GET("posts")
    suspend fun listPosts(
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 20
    ): List<Post>
}

Setting Up Retrofit with OkHttp

Configure HTTP client with interceptors:

val httpClient = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor())
    .addNetworkInterceptor(StethoInterceptor())
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(httpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
    .build()

val apiService = retrofit.create(ApiService::class.java)

Adding Authentication Interceptor

Automatically attach tokens to requests:

class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val token = tokenProvider.getAuthToken()

        val authenticatedRequest = originalRequest.newBuilder()
            .header("Authorization", "Bearer $token")
            .build()

        return chain.proceed(authenticatedRequest)
    }
}

// Add to OkHttpClient
httpClient.addInterceptor(AuthInterceptor(tokenProvider))

Error Handling and Retry Logic

Implement robust error handling:

class ErrorInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var response = chain.proceed(chain.request())

        if (response.code == 401) {
            // Token expired, refresh and retry
            refreshToken()
            response = chain.proceed(chain.request())
        }

        return response
    }
}

// Use with coroutines
viewModelScope.launch {
    try {
        val user = apiService.getUser("123")
        updateUI(user)
    } catch (e: HttpException) {
        showError("HTTP Error: ${e.code()}")
    } catch (e: IOException) {
        showError("Network Error")
    }
}

Retrofit simplifies API integration. Combine it with proper interceptor setup for production-ready networking.

8 Android app templates on Gumroad

Compose Stability and Recomposition Optimization — @Stable/@Immutable/skippable

2026-03-02 11:40:25

What You'll Learn

This article explains Compose stability (@stable, @Immutable, skippable judgment, Compose Compiler Report, and performance optimization).

Understanding Stability

Compose skips recomposition if arguments haven't changed. Whether skipping is possible depends on the "stability" of the arguments.

// ✅ Stable (auto-detected): primitives, String, function types
@Composable
fun Greeting(name: String) { // String = stable → skippable
    Text("Hello, $name")
}

// ❌ Unstable: List, Map and other collections
@Composable
fun UserList(users: List<User>) { // List = unstable → recomposed every time
    LazyColumn {
        items(users) { UserItem(it) }
    }
}

@Immutable

@Immutable
data class User(
    val id: String,
    val name: String,
    val email: String
)

// If all properties are val and stable types, add @Immutable
// Compose treats this class as guaranteed immutable

@stable

@Stable
class CounterState {
    var count by mutableIntStateOf(0)
        private set

    fun increment() { count++ }
}

// @Stable = "As long as equals() returns the same result for the same instance, skipping is possible"
// Use on classes containing MutableState

Stabilizing Collections

// Method 1: kotlinx.collections.immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

@Composable
fun UserList(users: ImmutableList<User>) { // ✅ Stable
    LazyColumn {
        items(users.size) { index -> UserItem(users[index]) }
    }
}

// Caller side
val users = usersList.toImmutableList()

// Method 2: Wrapper class
@Immutable
data class UserListWrapper(val users: List<User>)

Compose Compiler Report

// build.gradle.kts
android {
    composeCompiler {
        reportsDestination = layout.buildDirectory.dir("compose_reports")
        metricsDestination = layout.buildDirectory.dir("compose_metrics")
    }
}
// Example report output
// app_release-composables.txt
restartable skippable scheme("[androidx.compose.ui.UnitComposable]") fun Greeting(
  stable name: String
)

restartable scheme("[androidx.compose.ui.UnitComposable]") fun UserList(
  unstable users: List<User>  // ← Unstable! Not skippable
)

derivedStateOf

@Composable
fun FilteredList(items: List<Item>, query: String) {
    // ❌ Recomputed every time
    val filtered = items.filter { it.name.contains(query) }

    // ✅ Recomputed only when result changes
    val filtered by remember(items, query) {
        derivedStateOf { items.filter { it.name.contains(query) } }
    }

    LazyColumn {
        items(filtered) { item -> ItemRow(item) }
    }
}

Stabilization with key

@Composable
fun ItemList(items: List<Item>) {
    LazyColumn {
        items(
            items = items,
            key = { it.id } // ✅ key guarantees identity → reused
        ) { item ->
            ItemRow(item)
        }
    }
}

remember + lambda stabilization

// ❌ New lambda instance created every time
@Composable
fun Parent(viewModel: MyViewModel) {
    Child(onClick = { viewModel.doSomething() })
}

// ✅ remember stabilizes lambda
@Composable
fun Parent(viewModel: MyViewModel) {
    val onClick = remember(viewModel) { { viewModel.doSomething() } }
    Child(onClick = onClick)
}

Summary

Technique Use Case
@Immutable Immutable data classes
@Stable Classes with MutableState
ImmutableList Stabilize collections
derivedStateOf Optimize derived state
key LazyList item identity
Compiler Report Discover unstable args
  • Use Compose Compiler Report to find unstable arguments
  • Mark stability with @Immutable/@Stable
  • Stabilize collections with kotlinx.collections.immutable
  • Prevent unnecessary recalculation with derivedStateOf

8 production-ready Android app templates (performance optimized) are available.

Browse templatesGumroad

Related articles:

  • Recomposition debugging
  • LazyColumn optimization
  • Baseline Profile

Ready-Made Android App Templates

8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.

Browse templatesGumroad

pgwd: A Watchdog for Your PostgreSQL Connections

2026-03-02 11:37:45

Stop guessing when your database is about to run out of connections.

You’ve seen it before: an app starts failing with "sorry, too many clients already", and you only notice when users complain. By then, the database is saturated, and even your admin tools can’t connect. pgwd (Postgres Watch Dog) is a small Go CLI that watches your connection counts and alerts you before you hit the limit—and when you can’t connect at all.

The problem

PostgreSQL has a max_connections limit. When you exceed it:

  • New connections are rejected with FATAL: sorry, too many clients already (SQLSTATE 53300).
  • If your app uses a superuser (or a role that can use all slots), even DBA access can be blocked unless you’ve reserved slots with superuser_reserved_connections.

Without something watching connection usage, you only find out when things are already broken.

Flow: PostgreSQL → pgwd (thresholds) → Slack / Loki
How pgwd fits in: it watches your Postgres and pushes alerts to Slack and/or Loki when thresholds are exceeded or the connection fails.

The idea: watch and alert

pgwd connects to your Postgres (directly or via Kubernetes), reads connection stats from pg_stat_activity, and sends alerts to Slack and/or Loki when:

  • Total or active connections cross a threshold (e.g. 80% of max_connections).
  • Idle or stale connections exceed limits (useful for spotting connection leaks).
  • The connection fails—including "too many clients"—so you get an urgent notification even when pgwd itself can’t connect.

So you get notified when you’re approaching the limit, and again when the instance is already saturated.

Example Slack alert when Postgres returns
When the DB is saturated, pgwd sends an urgent alert like this to your notifiers.

pgwd in action

Minimal setup (one-shot from cron)

# Alert when total connections >= 80% of server max_connections (default)
pgwd -db-url "postgres://user:pass@localhost:5432/mydb" \
     -slack-webhook "https://hooks.slack.com/services/..."

No need to set thresholds if you’re fine with 80%: pgwd reads max_connections from the server and applies a default percentage.

Daemon mode (continuous watch)

export PGWD_DB_URL="postgres://localhost:5432/mydb"
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/..."
export PGWD_INTERVAL=60

pgwd
# Runs every 60 seconds; exit with SIGTERM.

Catch connection leaks (stale connections)

pgwd -db-url "postgres://..." -slack-webhook "https://..." \
     -stale-age 600 -threshold-stale 1

Alerts when any connection has been open longer than 10 minutes—handy for spotting leaks or long-running transactions.

Postgres in Kubernetes

pgwd -kube-postgres default/svc/postgres \
     -db-url "postgres://user:DISCOVER_MY_PASSWORD@localhost:5432/mydb" \
     -slack-webhook "https://..."

pgwd runs kubectl port-forward, can read the password from the pod’s environment, and connects to localhost. Alerts include cluster/namespace/service context.

When things go wrong

If the database is unreachable or returns "too many clients", pgwd always sends an urgent alert to your notifiers (when configured)—no extra flag needed. So even in the worst case, you get a Slack/Loki message instead of silence.

Try it

  • Install: go install github.com/hrodrig/pgwd@latest
  • Repo and docs: github.com/hrodrig/pgwd
  • Releases: Releases (binaries, Docker image ghcr.io/hrodrig/pgwd)

One-shot, daemon, or cron—with Slack and/or Loki you can stop flying blind on connection usage and get ahead of "too many clients" before it hits production.

Disclosure: This post was drafted with AI assistance and reviewed by the author.

I built an application to stop YOU from blindly pasting AI slop into your project.

2026-03-02 11:35:43

AI agents are making us incredibly fast, but they're also making it dangerously easy to ship insecure code.

Students and junior devs blindly follow code suggestions from Copilot and ChatGPT, not even once thinking about the structural integrity of the code. They may or may not have SQL injections, exposed API keys, and severe architectural flaws.

We don't need AI to stop writing code for us. We need better tools to understand and verify what the AI wrote before it hits production.

So, I built Soteria.

What is it?

Soteria is an AI-powered code security platform built specifically for students and early-career developers. Think of it as an educational firewall. It doesn't just highlight vulnerabilities; it helps you build a mental model for secure coding.

Key Features:

  • Built to recognize 50+ languages, Soteria instantly detects injection flaws, XSS, and dozens of other vulnerability patterns.

  • Not aware of the problem at hand? No problem, every bug/vulnerability has a beginner-friendly, plain-English explanation of why the code is dangerous and exactly how to fix it.

  • The better you get at your security habits, the more XP you earn for every scan, level up your rank (from Novice to Architect), and build your security intuition over time.

We leverage the Gemini 2.5 Pro API for deep contextual analysis of code snippets. Instead of just running static regex checks, Soteria passes the code context to Gemini, which acts as the "Neural Engine" to explain why something is vulnerable and provide exact, context-aware remediation steps.

We also included a structured architecture with specialized engines (like the KnowledgeGraph and NeuralEngine) to parse API responses, structure the vulnerability data, and ensure the explanations are accurate and beginner-friendly.

Why I built this

The gap between "it works" and "it's secure" is massive. I wanted to create a tool that catches developers right at the moment of integration.

If we can teach developers to recognize a vulnerable pattern specifically when they are about to commit it, the amount of money and time saved is astronomical!

But for all this to happen, I need YOUR guys' help‼️

Sometimes developers have an oversight with their code (with bugs and features that could be a game-changer), and that's where users come in 👀.

SO GO TRY IT: trysoteria.live

Leave a comment down below or connect with me on LinkedIn