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.
Data Layer
The data layer provides data to the application in two parts.
- Data Sources — data from network / database / memory
- 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.
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 theoperator
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.
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 aViewModel
, and as such, can survive configuration changes. To scope a dependency to aViewModel
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.
Android & iOS in Common Architecture.
Good example of one project with a common file system and architecture one on both
Android
and iOS
Testing
Now that we have all the components to write the app we switch to everyones least favorite topic … testing 😿
Focusing on Jetpack Compose which are instrument tests (meaning they need the emulator or physical device).
The CodeLab!
And of course the cheatsheet …
https://developer.android.com/jetpack/compose/testing-cheatsheet
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 usingSemanticsPropertyReceiver
and use the property with thesemantics
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.
Roboelectric
Nothing much changes for Roboelectirc when using Compose but here is nice summery of how it works.
CI/CD
Again, nothing special to use CI/CD with Compose.
https://developer.android.com/studio/projects/continuous-integration
Having spent years building on mobile these are the important lessons learned:
- Native declarative / reactive development is the way to go.
- Android: Jetpack Compose / Kotlin Flow
- iOS: SwiftUI / Combine Framework / Swift Structured Concurrency
- Kotlin Multi Mobile — https://kotlinlang.org/lp/mobile/
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)