Super Fast Android / iOS Development with MAD Skills

Siamak (Ash) Ashrafi
12 min readMay 15, 2022

--

Tools needed to build Android / iOS Apps @ Speed of Thought!

Why build so fast?

Developer velocity is the key to innovation. The faster the develop, test, learn cycle is the better the final product. ~ Ash

Have been working on Android Compose / SwiftUI MVVM from the very start.

Android & iOS in the Same Mental Model

Android:

We use clean architecture and modularize by feature:

  • Kotlin (Coroutines, Flow / Channels)
  • Jetpack Compose / MaterialU (3)
  • Clean Architecture with MVI pattern
  • Hilt - for Dependency Injection pattern implementation
  • Room - for local database
  • Coil - for image loading
  • Ktor - for networking
  • Kotlin Serialization converter - for JSON parsing
  • Gradle Version Catalog - for dependency management
  • Modularized project by features
  • Compose test APIs
  • GitHub Actions (CI/CD pipeline)

iOS:

  • Swift (with structured concurrency)
  • SwiftUI / Apple HIG
  • Clean Architecture with MVVM pattern
  • Swinject - Dependency Injection
  • CoreData - for local database
  • Combine Framework
  • Swift Package Manager - for dependency management
  • Modularized workspaces by features
  • XCTest Framework
  • GitHub Actions (CI/CD pipeline)

Building Android

Modern Android Development (MAD)

To Build @ Speed of Thought you need MAD Skills

Development tools, APIs, language, and distribution technologies recommended by the Android team to help developers be productive and create better apps that run across billions of devices.

https://developer.android.com/modern-android-development

Video Series:

Google I/O 2022: Android and Play video playlist.

🚫 Java / ⛔️ XML

Kotlin

A better Java

  • Flow

Emit values sequentially over time.

Expose application data using Kotlin Flows. Used with RoomDB for a reactive MVVM architecture. Access local database using suspend functions (see below).

  • Channels

Transfer a stream of values via broadcasters and receivers.

Can be used with BLE to communicate with GATT server (see below).

KTX

Kotlin extensions to make Android programming more fun and productive.

KTX extensions provide concise, idiomatic Kotlin to Jetpack, Android platform, and other APIs.

https://developer.android.com/kotlin/ktx

Data Layer

The data layer provides data to the application in two parts.

  1. Data Sources — data from network / database / memory
  2. Repositories — prepare the data for the UI, centralize changes and resolve conflicts using business logic.

Notes:

  • You should have a repo for each type of data (i.e. drivers / stores / homes).
  • For best practices cache your network data. Try the networks and if it works update the local DB else use the local cached version.
  • Try to keep all your data immutable.
  • Any errors should provide the UI layer with meaningful information.
  • Calling the repository should be main safe (not block the UI thread).
  • For testing one can use the Room InMemory Database 👍🏾.
  • Also use Hilt dependency injection for testing.

RoomDB

Makes working with SQLite DB much better, easier and safer.

  • ROOM@Entity

A class that represents your data

@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
https://developer.android.com/training/data-storage/room#data-entity

https://developer.android.com/training/data-storage/room

  • DAO@Dao

Data Access Object (DAO)

SQL commands to access the data and using Kotlin Suspend / Flow (MVVM)

@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): Flow<List<User>>
@Insert
Suspend fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}

https://developer.android.com/training/data-storage/room/accessing-data

  • Entities — @Entity

Builds the Room DB

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

https://developer.android.com/training/data-storage/room/defining-data

Some good advice about using Room!

~~~

Using Compose and Kotlin Flow with RoomDB in MVVM / MVI

Room can provide Compose with realtime value updates using Kotlin Flow!

Compose ships with functions to create State<T> from common observable types used in Android apps: Flow

https://developer.android.com/jetpack/compose/state

This is the bases of building a MVVM / MVI application with unidirectional data flow.

A unidirectional data flow (UDF) is a design pattern where state flows down and events flow up.

source: https://developer.android.com/topic/architecture/ui-layer

https://developer.android.com/jetpack/compose/architecture

UI Layer

The UI is the visual state of the application. Updating the UI with user actions is the reflection of the app state not a sequence of events. So the state of the application is the state of the UI.

A Kotlin data class can be use used to represent the state of the app and this can also be used to drive the UI. You should never use the UI to update this state, but the UI should update the datasource (which updates the state).

ViewModel

Use a ViewModel to mange the flow of data between the UI and data source.

The ViewModel is the main app state holder. The UI notifies the ViewModel of events and the ViewModel updates the app state which updates the UI.

Using Kotlin Flow the UI knows the data has changed. So we never have to worry about the UI and data going out of sync because they are bound together.

https://developer.android.com/topic/libraries/architecture/viewmodel

Use cases (Optional)

Use case classes fit between ViewModels from the UI layer and repositories from the data layer.

  • Case class instances should be callable as functions by defining the invoke() function with the operator modifier.
  • Use cases don’t have their own lifecycle. Instead, they’re scoped to the class that uses them.
  • Use cases from the domain layer must be main-safe!
  • Encapsulate repeatable business logic present in the UI layer in a use case class.
  • If logic involves multiple repositories create a <Complex>UseCase class to abstract the logic out of the ViewModel and make it more readable. This also makes the logic easier to test in isolation, and reusable in different parts of the app.

IMPORTANT NOTE! The Room library lets you query relationships between different entities in a database. If the database is the source of truth, you can create a query that does all that work for you. In that case, it’s better to create a repository class and not a use case.

🛩 Compose

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.

https://developer.android.com/jetpack/compose

Modern declarative UI designed for reactive programming (MVVM / MVI)

https://developer.android.com/jetpack/compose

  • Permissions

The best way to do user permissions with Compose.

  • Material

Google material design built into Compose components.

https://developer.android.com/jetpack/compose/layouts/material

  • Animation

Easy & fast way to do animation in Compose.

https://developer.android.com/jetpack/compose/animation

  • Navigation

Use navigation components with Compose.

https://developer.android.com/jetpack/compose/navigation

Dependance Injection

Implementing dependency injection provides reusability of code, ease of refactoring and ease of testing.

Dagger Hilt

We need a way to build our objects. Without a way to build we can’t do anything …

Dependency injection (IoC) with Hilt.

Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project.

https://developer.android.com/training/dependency-injection/hilt-android

  • Application Class@HiltAndrroidApp

Setup the Android entry point

All apps that use Hilt must contain an Application class that is annotated with @HiltAndroidApp.

https://developer.android.com/training/dependency-injection/hilt-android#application-class

……………. Using Hilt with Jetpack Libs ..……………

Hilt includes extensions for providing classes from other Jetpack libraries: ViewModel,Navigation, Compose and WorkManager.

https://developer.android.com/training/dependency-injection/hilt-jetpack

  • ModelView — @HiltViewModel / @ViewModelScoped

A Hilt View Model is a Jetpack ViewModel that is constructor injected by Hilt.

We use Hilt to get our ViewModels (@HiltViewModel)

@ViewModelScoped — All Hilt ViewModels are provided by the ViewModelComponent which follows the same lifecycle as a ViewModel, and as such, can survive configuration changes. To scope a dependency to a ViewModel use the @ViewModelScoped annotation.

https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodels

  • Navigation

We use Hilt to get our ViewModels (@HiltViewModel) inside a @Composable

If your @HiltViewModel annotated ViewModel is scoped to the navigation graph, use the hiltViewModel composable function that works with activities that are annotated with @AndroidEntryPoint.

@Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
navigation(startDestination = innerStartRoute, route = "Parent") {
// ...
composable("exampleWithRoute") { backStackEntry ->
val parentEntry = remember {
navController.getBackStackEntry("Parent")
}
val parentViewModel = hiltViewModel<ParentViewModel>(
parentEntry
)
ExampleWithRouteScreen(parentViewModel)
}
}
}
}
  • Compose — @HiltViewModel

The viewModel() function mentioned in the ViewModel section automatically uses the ViewModel that Hilt constructs with the @HiltViewModel annotation.

@HiltViewModel
class ExampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() { /* ... */ }
@Composable
fun ExampleScreen(
exampleViewModel: ExampleViewModel = viewModel()
) { /* ... */ }
  • WorkManager — @HiltWorker

Setup to inject WorkManager with Hilt

@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
workerDependency: WorkerDependency
) : Worker(appContext, workerParams) { ... }

In the App

@HiltAndroidApp
class ExampleApplication : Application(), Configuration.Provider {

@Inject lateinit var workerFactory: HiltWorkerFactory

override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

https://developer.android.com/training/dependency-injection/hilt-jetpack#workmanager

A thought about directory structure 💭 …

We organize everything by features!

A feature is a set of one or multiple screens. Example: login feature, setting preference feature, setting alarm feature, taking picture feature, ordering item feature, returning item feature etc …

Features should be easy to replace.

We have 3 Layers in each feature :-)

  • Presentation layer — the UI
  • Domain Layer — the business logic
  • Data Layer — Database access / Network access

Structure

  • presentation — screen_#, screen_#, util and MainActivity.kt
  • domain — model, repository, use_cases and util
  • data — data_source and repository

Below is an example directory structure using this layout from Clean Architecture MVVM Note App Video.

→ Presentation directory has a directory for each screen.

Really like this structure because it is very logically laid out.

Current Dev Env

Android & iOS in Common Architecture.

Good example of one project with a common file system and architecture one on both

Android

and iOS

Directory structure:

  • Unit tests go into the test directory. <com(test).app.name.unit test>. These will be your use cases …
  • Compose test go into the AndroidTest directory because they are instrument tests (🤔 Espresso). <com(androidTest).app.name.UI test>

Semantics: The semantic tree is used for UI testing in compose and we have three main ways to interact with the elements:

  • Finders — select one or multiple elements using onNode (use cheatsheet).
  • Assertions — verify elements attributes
  • Actions — user events (clicks or gestures)

Note: Add properties to the semantic tree by adding the modifier.

TestButton ( modifier = Modifier.semantic{contentDscription = "TB"}

Note: You can print the semantic tree with

composeTestRule.onRoot().printToLog("TAG") // useUnmergedTree = true

Matchers:

  • Hierarchical — up and down semantic tree preforming simple matches
  • Selectors — alternative way which is more readable (see cheatsheet)

Synchronization:

  • Compose app is advanced in time using a virtual clock.
  • You can disable automatic synchronization and advance the time yourself
  • You can use `waitForIdle` to do manual synchronization.
  • You can use advanceTimeUntil() to advance the clock until a certain condition is met

Common Patterns:

  • Testing in isolation — composables are encapsulated and independent.
  • Custom semantics properties* — define a new SemanticsPropertyKey , make it available using SemanticsPropertyReceiver and use the property with the semantics modifier.
val PickedDateKey = SemanticsPropetyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickDate by PickedDateKey

*Note: This should be avoided because it pollutes the production app!

Interoperability with Espresso:

With Compose app we do not need to use Espresso Instrumentation Testing unless it is a hybrid app. If that is the case then the author would ask you to convert it to a fully Compose app … LOL :-) ⛔️ Java / 🚫 XML

Compose Testing vs Espresso

Debugging

  • Print the semantic tree
findRoot().printToLog() // at any point in the test

Again, do the codelab covering: testing in isolation, debugging tests, semantics trees and synchronization.

https://developer.android.com/codelabs/jetpack-compose-testing#0

You code is placed in the directory: app/src/androidTest/com/<company>/<app>/<component> mirroring you production directory structor.

We use setContent method of the ComposeTestRule to call the composable.

When building our Composables we build them so that the @Preview works which means they can be tested in isolation because @Preview runs in isolation.

Testing Compose animations are built into the testing framework (i.e. infinite animations are a special case that Compose tests understand so they’re not going to keep the test busy).

Video Covering Everything using Compose!

Testing Notes:

  • The RoomDB has an in memory database for testing `Room.inMemoryDatabaseBuilder`
  • Use back-tick `good name for my test` to set good names on your tests
  • Integration tests: use the composeRule.onNode<function>, @HiltAndroidTest, custom test runner, and set the order.

Having spent years building on mobile these are the important lessons learned:

  1. Native declarative / reactive development is the way to go.

2. Bluetooth is very important

  • Kotlin makes BLE on Android much easier
  • BLE Android App

3. True advancement will be made by combining:

  • Mobile sensors
  • Machine Learning
  • Understanding physiological data/state of a person.

Also think a lot about design. Currently writing an entire article about how we design app icons.

https://zoewave.medium.com/iconification-the-study-of-app-icons-ead04db3a25f (work in progress)

Google Play Store

All Android Apps on Google Play Store follow the MAD architecture.

Here we cover the tech used to create the apps on the Google Play Store.

All apps are MVVM (Room, Repo, ModelView, Kotlin Flow) with Dagger Hilt and 100% pure Compose(UI, Animation, Navigation)

RxTack

Tech Used::

  • Text to speech
  • Speech to Text
  • CameraX

Photodo

Tech Used::

  • CameraX

TimeMap

Tech Used:

  • Compose Map
  • Compose permissions

Swift Bike Shift

BLE using Jetpack Compose / Kotlin Flow

Tech Used::

  • Kotlin Channels & Flow
  • Bluetooth Low Energy (see below)

Breathe Time (WearOS)

  • Compose WearOS

GoSwift Watch Face (WearOS)

___________________________________________________________________

Next we make it modularized!

Building iOS (Coming soon)

Modern Apple (iOS) Development (MAD)

Apple iOS Store (Coming soon)

Teaching — Speaking (Coming Soon)

--

--