I am a newbie in mobile development and I am trying to build my first Android app using Kotlin and Jetpack Compose. The actual app should just scan the visible access points and display them in a list. Even if the app is very simple, I want to follow the recommended app architecture, so I am following the following guide from the official docs. This means that I have my UI layer with composables and a ViewModel
(my state holder), and my Data layer with the business logic.
The problem I am encountering is that I need to register a broadcast listener and then request a scan (see this), but for that I need to use context
, which is not available in my ViewModel nor in my Repository class. Also I cannot find what the best approach is to pass the scan results down to the ViewModel.
This is my simple UI (MainActivity):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AccessPointsListScreen()
}
}
}
}
@Composable
fun AccessPointsListScreen(
accessPointsViewModel: AccessPointsViewModel = viewModel())
) {
// State to hold the scan results
val accessPointsUiState by accessPointsViewModel.uiState.collectAsState()
AccessPoints(
accessPointsList = accessPointsUiState.accessPointsList
)
}
@Composable
private fun AccessPoints(
accessPointsList: List<AccessPoint>
) {
// Omit implementation of UI
}
Here is my ViewModel class:
/**
* UI state for the Access Points
*/
data class AccessPointsUiState(
val accessPointsList: List<AccessPoint> = emptyList()
)
/**
* ViewModel that handles the logic of the access points screen
*/
class AccessPointsViewModel(
private val accessPointsRepository: AccessPointsRepository
): ViewModel() {
private val _uiState = MutableStateFlow(AccessPointsUiState())
val uiState: StateFlow<AccessPointsUiState> = _uiState.asStateFlow()
init {
refreshAccessPoints()
}
/**
* Refresh access points and update the UI state
*/
fun refreshAccessPoints() {
viewModelScope.launch {
val result = accessPointsRepository.scanAccessPoints()
// Omit logic to update state
}
}
}
and finally my Repository class (Data layer):
class AccessPointsRepository {
suspend fun scanAccessPoints(): List<ScanResult> {
// How do I proceed here??
val wifiScanReceiver: BroadcastReceiver = WifiScanReceiver() // Should I instantiate this here??
// Register a broadcast listener for SCAN_RESULTS_AVAILABLE_ACTION, which is called when scan requests are completed
registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) // This is incorrect, I need the context
val success = wifiManager.startScan() // This won't work without initializing wifiManager, for which I need the context
}
}
class WifiScanReceiver: BroadcastReceiver() {
var scanResultList = mutableListOf<ScanResult>()
lateinit var wifiManager: WifiManager // Where do I initialize this if I need the context??
override fun onReceive(context: Context, intent: Intent) {
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
if (!success) {
// Omit
}
scanResultList = wifiManager.scanResults // wifiManager is not initialized
// Where and how do I return the scanResultList??
}
}
So, how do I use/create my Broadcast receiver in my AccessPointsRepository
class if I don't have the context? Is it well placed there or would it fit better in the ViewModel? I was able to make it work when I was writing everything in the MainActivity
file, so the problem is not with the implementation itself but more about what the best approach is in a clean architecture that keeps the separation of concerns.I saw that AndroidViewModel
has a reference to the application context, but it looks like using it isn't a good practice.
I also don't see how I should pass the List<ScanResult>
from the onReceive
method down to my ViewModel
class. It feels like I am doing some things fundamentally wrong.
Could someone provide any guidance on how should I proceed while following the best architectural principles? Thanks in advance guys!
how do I use/create my Broadcast receiver in my AccessPointsRepository class if I don't have the context?
You cannot, so you need to give the repository a Context
:
Define the API for your repository as an interface
Use your dependency inversion framework (Dagger/Hilt, Koin, kotlin-inject, etc.) to supply an Application
to the repository implementation of that interface
Have the rest of your app consume the repository by requesting the interface from the dependency inversion framework
Use the interface to create a test fake when you write unit tests involving the repository
Is it well placed there or would it fit better in the ViewModel?
A repository or something similar in the data layer seems to be a reasonable choice.
I saw that
AndroidViewModel
has a reference to the application context, but it looks like using it isn't a good practice.
I would describe it more as "know why you are using AndroidViewModel
and its Context
". There is a very good chance that you will want the Context
access to be elsewhere, but there may well be some good reasons to use it in a viewmodel from time to time.
I also don't see how I should pass the List from the onReceive method down to my ViewModel class.
Off the cuff, I would have the repository interface expose a Flow
. Have the repository implementation implement that via a MutableStateFlow
. Update the MutableStateFlow
with the now-current List
as you get results in from the scan. The viewmodel can observe the Flow
. You trigger the scan (or a later re-scan, such as via pull-to-refresh) by calling a function on the repository.
Over time, you can then get more sophisticated. For example, you could wrap the List
in something that can help distinguish "we have no access points because we have not scanned yet" from "we have no access points, because we cannot find any" from "we have no access points because our last scan was too long ago and we consider the results to be stale".