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
Android
Kotlin + Jetpack Compose, the official option. Java or Kotlin with Views is the old world: you will find it in legacy code.
iOS
Swift + SwiftUI. UIKit is the older option. You need a Mac to build and to publish.
Flutter
Dart and a single codebase for Android, iOS, web and desktop. Plenty of packages on pub.dev and good Firebase or Supabase integration.
React Native
JavaScript or TypeScript with React. Ionic and Cordova are more web-based alternatives.
Kotlin Multiplatform
Logic in Kotlin shared between Android and iOS. With Compose Multiplatform you share the UI too.
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"
class Station {
final int id;
final String name;
final int freeBikes;
const Station({required this.id, required this.name, required this.freeBikes});
}
String label(Station? s) => s?.name ?? 'Unknown';
struct Station: Codable {
let id: Int
let name: String
let freeBikes: Int
}
func label(_ s: Station?) -> String { 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)
The binder is the reactive link: StateFlow, LiveData, Combine. The view observes the state and redraws itself (Observer pattern).
BLoC (Flutter)
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) }
}
}
class StationsBloc {
StationsBloc(this.source);
final StationsDataSource source;
final stations = ValueNotifier<List<Station>>([]);
Future<void> load() async {
final list = await source.getStations();
stations.value = [...list]..sort((a, b) => a.name.compareTo(b.name));
}
}
class StationsPage extends StatelessWidget {
const StationsPage({super.key});
@override
Widget build(BuildContext context) {
final bloc = locator<StationsBloc>();
return ValueListenableBuilder(
valueListenable: bloc.stations,
builder: (context, stations, _) => ListView(
children: [for (final s in stations) StationTile(station: s)],
),
);
}
}
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 analyzewith theflutter_lintspackage, plusdart 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
name: Flutter CI
on:
pull_request:
push:
branches: [develop, main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: dart format --output=none --set-exit-if-changed .
- run: flutter analyze
- run: flutter test --coverage
- run: flutter build apk --debug
Getting the app to teachers and stakeholders
Firebase App Distribution
Email each build to whoever should test it, Android and iOS. The easiest option for PES.
TestFlight
For iOS. Requires the Apple Developer Program.
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.
- Always review, test and validate generated code. If you do not understand it, do not merge it.
- If you use them for engineering tasks, explain the process in the report: it is one of the assessed methodological aspects.
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.
BicingAround
Android: Kotlin, Jetpack Compose and MVVM.
bicing-around-flutter
Flutter: Dart and the BLoC architecture.
Kotlin and Android
- Kotlin Koans
- Android Basics with Compose
- ViewModel (MVVM)
- Retrofit, for REST requests
- The Clean Architecture
Flutter and Dart
- Flutter docs and the cookbook
- Dart and DartPad, to try code in the browser
- Flutter architectures compared