Developing a Mobile App for HR and Recruiting
An HR app serves two fundamentally different scenarios simultaneously: recruiters manage candidate funnels and schedule interviews, while new employees go through onboarding and fill documents. Both scenarios require different UX patterns, different access rights, and different push notifications. Most architectural decisions arise at this intersection.
Role-Based Model and Architecture
Minimum three roles: HR Manager/Recruiter, Candidate, Employee (after offer). Each has its own navigation flow and set of screens.
// iOS — NavigationRouter based on role
enum AppRole {
case recruiter
case candidate
case employee
}
func buildRootNavigation(for role: AppRole) -> UIViewController {
switch role {
case .recruiter: return RecruiterTabBarController()
case .candidate: return CandidateOnboardingFlow()
case .employee: return EmployeePortalController()
}
}
User role comes in JWT token after login. When role changes (candidate becomes employee) — rebuild navigation without app restart.
Candidate Funnel: Kanban for Recruiters
Recruiting funnel is Kanban: New → Phone Screening → Technical Interview → Offer → Rejection. On mobile — horizontally scrollable columns.
// Android — funnel state via ViewModel
data class RecruitingFunnelState(
val stages: List<FunnelStage>,
val selectedStageIndex: Int = 0,
val candidates: Map<String, List<Candidate>> // stageId → candidates
)
class FunnelViewModel(private val repo: CandidateRepository) : ViewModel() {
val state = MutableStateFlow(RecruitingFunnelState(stages = emptyList()))
fun moveCandidateToStage(candidateId: String, targetStageId: String) {
viewModelScope.launch {
repo.updateCandidateStage(candidateId, targetStageId)
// Optimistic UI update
state.update { current ->
val updated = current.candidates.toMutableMap()
// move candidate between lists
current.copy(candidates = updated)
}
}
}
}
Drag-and-drop between columns — via ItemTouchHelper with custom ViewHolder.onDragOver. Simpler — tap-to-move via bottom sheet with stage selection.
Interview Scheduling
Interview requires: slot selection, interviewer invitation, meeting link send, 15-minute push reminder.
Integration with Google Calendar via Google Calendar API or CalDAV, with Microsoft via Graph API. Mobile client requests available slots:
func fetchAvailableSlots(interviewerId: String, date: Date) async throws -> [TimeSlot] {
// Request to backend that aggregates Google Calendar + internal calendar
return try await api.getAvailableSlots(interviewerId: interviewerId, date: date)
}
Push reminder scheduled when interview created — scheduled notification via FCM or OneSignal with deliver_after.
Employee Onboarding
Onboarding is sequential flow: fill personal data, upload documents (passport, ID, employment contract), review company policies, complete first-day tasks.
Onboarding progress — checklist with status of each item. Server stores JSON schema of onboarding (set of steps), client renders it dynamically:
data class OnboardingStep(
val id: String,
val type: StepType, // FORM, DOCUMENT_UPLOAD, QUIZ, VIDEO, TASK
val title: String,
val isRequired: Boolean,
val completedAt: Long?
)
enum class StepType { FORM, DOCUMENT_UPLOAD, QUIZ, VIDEO, TASK }
Document upload — multipart form data with progress:
func uploadDocument(_ data: Data, type: DocumentType) async throws -> DocumentId {
return try await api.upload(
endpoint: "/employee/documents",
file: data,
filename: "\(type.rawValue)_\(Date().timestamp).pdf",
mimeType: "application/pdf"
) { progress in
self.uploadProgress = progress
}
}
Push Notifications in HR Context
| Event | Recipient | Priority |
|---|---|---|
| New vacancy response | HR Manager | High |
| Interview scheduled | Candidate + Interviewer | High |
| Interview reminder (15 min) | All participants | Critical |
| Offer sent to candidate | Candidate | High |
| Onboarding step requires action | New employee | Medium |
| Probation period expires | HR Manager | Medium |
Corporate Communication
Internal chat or integration with existing tools (Slack API, Microsoft Teams webhooks). For MVP — internal chat via WebSocket:
class HRChatSocket(private val token: String) {
private val client = OkHttpClient()
private var ws: WebSocket? = null
fun connect(roomId: String, onMessage: (ChatMessage) -> Unit) {
val request = Request.Builder()
.url("wss://api.yourhr.app/ws/chat/$roomId")
.header("Authorization", "Bearer $token")
.build()
ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
onMessage(json.decodeFromString(text))
}
})
}
}
Timeline
MVP HR app (candidate funnel, candidate cards, basic onboarding, push notifications) — 10–14 weeks. Full functionality with interview scheduler, calendar integrations, corporate chat, and analytics — 20–28 weeks.







