Guia PES

Mobile app

Pick a technology and build an architecture that lasts three sprints

No technology is mandatory. What counts is a justified decision and code that is clean, tested and easy to change when the backend or the other team's service changes.

The options

Native

Android

Kotlin + Jetpack Compose, the official option. Java or Kotlin with Views is the old world: you will find it in legacy code.

Native

iOS

Swift + SwiftUI. UIKit is the older option. You need a Mac to build and to publish.

Cross-platform

Flutter

Dart and a single codebase for Android, iOS, web and desktop. Plenty of packages on pub.dev and good Firebase or Supabase integration.

Web-based cross-platform

React Native

JavaScript or TypeScript with React. Ionic and Cordova are more web-based alternatives.

Shared native

Kotlin Multiplatform

Logic in Kotlin shared between Android and iOS. With Compose Multiplatform you share the UI too.

How to choose

Key questions

  • What does the team already know?
  • Do you need iOS, and do you have a Mac?
  • Are there libraries for what you need (maps, social login, chat)?
  • Do you justify it in the report?

The same model in three languages

data class Station(val id: Int, val name: String, val freeBikes: Int)

// null safety: if there is no station, a default text
fun label(s: Station?) = s?.name ?: "Unknown"

Two architectures that keep the UI apart from the logic

The rule is the same: the view never talks to the API directly. That way you can test the logic without screens and swap the data source (mock or real) without touching the UI.

MVVM (Android, iOS)

View
binder
ViewModel
call
Model

The binder is the reactive link: StateFlow, LiveData, Combine. The view observes the state and redraws itself (Observer pattern).

BLoC (Flutter)

UI
events / states
BLoC
request
Data

Events come in (a tap, a screen opening), the BLoC processes them and emits states that the UI paints.

class StationsViewModel(private val source: StationsDataSource) : ViewModel() {
    private val _stations = MutableStateFlow<List<Station>>(emptyList())
    val stations: StateFlow<List<Station>> = _stations

    init {
        viewModelScope.launch { _stations.value = source.getStations().sortedBy { it.name } }
    }
}

@Composable
fun StationsScreen(vm: StationsViewModel = koinViewModel()) {
    val stations by vm.stations.collectAsStateWithLifecycle()
    LazyColumn {
        items(stations) { StationRow(it) }
    }
}

For larger projects, the flutter_bloc package makes events and states explicit. To get started, a ValueNotifier already separates logic from UI.

Linters: code ready to show

A linter analyses code without running it and finds bugs and style problems. The goal is clean code, like a document someone else will have to read. It counts towards quality management.

  • Kotlin: ktlint (style) and detekt (code smells).
  • Swift: SwiftLint.
  • Dart: flutter analyze with the flutter_lints package, plus dart format.
  • JavaScript or TypeScript: ESLint and Prettier.
  • All of it: SonarQube or SonarCloud to track it over the course.

A test the mock makes possible

test('sorts stations by name', () async {
  final bloc = StationsBloc(FakeStationsDataSource());
  await bloc.load();
  final names = bloc.stations.value.map((s) => s.name).toList();
  expect(names, [...names]..sort());
});

No network, and it runs in milliseconds. Generate the fake data source with the generator and add coverage to the report (flutter test --coverage, Kover or JaCoCo for Kotlin).

Continuous integration with GitHub Actions

Every pull request runs the linter, the tests and the build before it is merged. That is the configuration management that gets assessed. Save the file as .github/workflows/ci.yml. Alternatives: GitLab CI, Jenkins, Codemagic or Azure DevOps.

name: Android CI
on:
  pull_request:
  push:
    branches: [develop, main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'
      - name: Lint, tests and build
        run: |
          chmod +x gradlew
          ./gradlew lint testDebugUnitTest assembleDebug
      # with the ktlint and detekt plugins: ./gradlew ktlintCheck detekt

Getting the app to teachers and stakeholders

Private · free

Firebase App Distribution

Email each build to whoever should test it, Android and iOS. The easiest option for PES.

Private

TestFlight

For iOS. Requires the Apple Developer Program.

Stores

Google Play and App Store

Google Play's internal testing track is a good first step. There are alternative stores too, such as F-Droid.

For the final delivery, the release information must explain step by step how to run the app. An APK on GitHub Releases plus a distribution link makes it very easy. See Fag-5 →

AI assistants: yes, but with your eyes open

Gemini, Copilot, Claude and similar tools help with planning and design (architecture drafts), generating boilerplate code, debugging (explaining errors, refactoring) and automating workflows with agents.

Examples and resources

Two implementations of the same Bicing stations app, shown live in the course's mobile development session. Look at how they separate UI, logic and data.

Flutter and Dart