androidkotlinandroid-jetpack-composebroadcastreceiver

Use of BroadcastReceiver in Android app's Data layer


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!


Solution

  • 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:

    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".