Skip to main content

Overview

GroupRepository handles all group-related data operations including creating groups, fetching group lists, updating group details, and deleting groups. Interface: backend/GroupRepository.kt:12
Implementation: backend/SupabaseGroupRepository.kt:45

Interface Definition

interface GroupRepository {
    fun listGroups(): Flow<DataResult<List<Group>>>
    fun getGroup(groupId: String): Flow<Group>
    suspend fun createGroup(name: String, icon: GroupIcon): Group
    suspend fun updateGroup(groupId: String, name: String, icon: GroupIcon)
    suspend fun deleteGroup(groupId: String)
    suspend fun refreshGroups()
}

Implementation

SupabaseGroupRepository

@Singleton
class SupabaseGroupRepository @Inject constructor(
    private val supabaseClient: SupabaseClient
) : GroupRepository {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val _groups = MutableStateFlow<DataResult<List<Group>>>(DataResult.Loading)
    
    init {
        scope.launch { refreshGroups() }
    }
}
The repository maintains an internal state flow that’s initialized to Loading and automatically refreshes on creation.

Methods

listGroups

Returns a reactive Flow of all groups for the current user.
fun listGroups(): Flow<DataResult<List<Group>>>
Returns: Flow<DataResult<List<Group>>> - Stream of group list with loading/error states Example Usage:
@HiltViewModel
class GroupsViewModel @Inject constructor(
    private val groupRepository: GroupRepository
) : ViewModel() {
    val groups = groupRepository.listGroups()
        .stateIn(viewModelScope, SharingStarted.Lazily, DataResult.Loading)
}
Implementation:
override fun listGroups(): Flow<DataResult<List<Group>>> = _groups

getGroup

Returns a Flow for a single group by ID.
fun getGroup(groupId: String): Flow<Group>
groupId
String
required
The unique identifier of the group to retrieve
Returns: Flow<Group> - Stream of the requested group (returns placeholder if not found) Example Usage:
val group = groupRepository.getGroup("group-123")
    .collectAsState(initial = Group(id = "group-123", name = "Loading..."))
Implementation:
override fun getGroup(groupId: String): Flow<Group> =
    _groups.map { result ->
        when (result) {
            is DataResult.Success -> result.data.find { it.id == groupId }
                ?: Group(id = groupId, name = "Group")
            else -> Group(id = groupId, name = "Group")
        }
    }

createGroup

Creates a new group with the current user as owner.
suspend fun createGroup(name: String, icon: GroupIcon): Group
name
String
required
The display name for the new group
icon
GroupIcon
required
The icon to display for the group (e.g., GroupIcon.Home, GroupIcon.Flight)
Returns: Group - The newly created group object Throws: Exception if Supabase call fails Example Usage:
viewModelScope.launch {
    try {
        val group = groupRepository.createGroup(
            name = "Weekend Trip",
            icon = GroupIcon.Flight
        )
        println("Created group: ${group.id}")
    } catch (e: Exception) {
        showError("Failed to create group: ${e.message}")
    }
}
Supabase Implementation:
override suspend fun createGroup(name: String, icon: GroupIcon): Group {
    val params = buildJsonObject {
        put("p_name", name)
        put("p_icon", icon.name)
    }
    val row = supabaseClient.postgrest
        .rpc("create_group_with_owner", params)
        .decodeSingle<GroupRow>()

    val group = Group(
        id = row.id,
        name = row.name,
        icon = iconFromName(row.icon),
        memberCount = 1,
        balanceCents = 0L,
        createdBy = row.createdBy
    )
    
    // Update local state
    _groups.update { result ->
        val current = (result as? DataResult.Success)?.data ?: emptyList()
        DataResult.Success(current + group)
    }
    
    return group
}
RPC Function: create_group_with_owner - Creates the group and adds the current user as a member

updateGroup

Updates an existing group’s name and icon.
suspend fun updateGroup(groupId: String, name: String, icon: GroupIcon)
groupId
String
required
The ID of the group to update
name
String
required
The new name for the group
icon
GroupIcon
required
The new icon for the group
Example Usage:
viewModelScope.launch {
    groupRepository.updateGroup(
        groupId = "group-123",
        name = "Summer Vacation",
        icon = GroupIcon.Beach
    )
}
Supabase Implementation:
override suspend fun updateGroup(groupId: String, name: String, icon: GroupIcon) {
    supabaseClient.from("groups").update({
        set("name", name)
        set("icon", icon.name)
    }) {
        filter { eq("id", groupId) }
    }
    
    // Update local state
    _groups.update { result ->
        val current = (result as? DataResult.Success)?.data ?: return@update result
        DataResult.Success(current.map { g ->
            if (g.id == groupId) g.copy(name = name, icon = icon) else g
        })
    }
}

deleteGroup

Deletes a group and all associated data (expenses, splits, members).
suspend fun deleteGroup(groupId: String)
groupId
String
required
The ID of the group to delete
Warning: This operation is irreversible and cascades to all related data. Example Usage:
viewModelScope.launch {
    try {
        groupRepository.deleteGroup("group-123")
        navigateBack()
    } catch (e: Exception) {
        showError("Failed to delete group: ${e.message}")
    }
}
Supabase Implementation:
override suspend fun deleteGroup(groupId: String) {
    val params = buildJsonObject { put("p_group_id", groupId) }
    supabaseClient.postgrest.rpc("delete_group_cascade", params)
    
    // Update local state
    _groups.update { result ->
        val current = (result as? DataResult.Success)?.data ?: return@update result
        DataResult.Success(current.filter { g -> g.id != groupId })
    }
}
RPC Function: delete_group_cascade - Deletes the group and all related records

refreshGroups

Forces a refresh of the groups list from Supabase.
suspend fun refreshGroups()
Example Usage:
viewModelScope.launch {
    groupRepository.refreshGroups()
}
Supabase Implementation:
override suspend fun refreshGroups() {
    try {
        val rows = supabaseClient.postgrest
            .rpc("get_my_groups_summary")
            .decodeList<GroupSummaryRow>()

        _groups.value = DataResult.Success(rows.map { row ->
            Group(
                id = row.id,
                name = row.name,
                icon = iconFromName(row.icon),
                memberCount = row.memberCount.toInt(),
                balanceCents = row.balanceCents,
                createdBy = row.createdBy
            )
        })
    } catch (e: Exception) {
        _groups.value = DataResult.Error("Failed to load groups", e)
    }
}
RPC Function: get_my_groups_summary - Returns groups with member counts and balance calculations

Data Models

Group

Source: models/Group.kt:7
@Serializable
data class Group(
    val id: String,
    val name: String,
    val emoji: String = "",
    val icon: GroupIcon = GroupIcon.Group,
    val memberCount: Int = 0,
    val balanceCents: Long = 0L,
    val currency: String = "USD",
    val createdBy: String = ""
) {
    val isOwed: Boolean get() = balanceCents >= 0
    val formattedBalance: String
    val balanceLabel: String
}

Internal Models

@Serializable
private data class GroupSummaryRow(
    val id: String = "",
    val name: String = "",
    val icon: String = "",
    @SerialName("created_by")   val createdBy: String = "",
    @SerialName("created_at")   val createdAt: String = "",
    @SerialName("member_count") val memberCount: Long = 0L,
    @SerialName("balance_cents") val balanceCents: Long = 0L
)

@Serializable
private data class GroupRow(
    val id: String = "",
    val name: String = "",
    val icon: String = "",
    @SerialName("created_by") val createdBy: String = "",
    @SerialName("created_at") val createdAt: String = ""
)

Error Handling

Error States

The repository wraps all Supabase operations in try-catch blocks and updates the state flow:
try {
    val data = fetchFromSupabase()
    _groups.value = DataResult.Success(data)
} catch (e: Exception) {
    _groups.value = DataResult.Error("Failed to load groups", e)
}

Common Errors

  • Network errors: Connection timeout, no internet
  • Authentication errors: Invalid session, expired token
  • Database errors: Constraint violations, RLS policy failures

See Also

Build docs developers (and LLMs) love