এই আর্টিকেলটি একটি প্রোডাকশন-লেভেল WiFi Scanner App বানানোর 엔্ড-টু-এন্ড গাইড। এটি পূর্বের দুটি আর্টিকেলের (1006061517, 1006061518) ভিত্তিতে তৈরি, কিন্তু আর্কিটেকচার, MVVM প্যাটার্ন, Flow/Coroutines, এবং Jetpack Compose UI ব্যবহার করে আধুনিক করা হয়েছে।
আর্কিটেকচার ওভারভিউ (MVVM + Repository Pattern)
com.example.wifiscannerpro
├── data/
│ ├── WifiRepository.kt # ডেটা লেয়ার (WifiManager wrapping)
│ └── model/
│ └── WifiNetwork.kt # ডেটা ক্লাস
├── ui/
│ ├── MainViewModel.kt # UI স্টেট ম্যানেজমেন্ট
│ ├── MainActivity.kt # Compose UI / Activity
│ └── adapter/
│ └── WifiNetworkAdapter.kt # RecyclerView Adapter (যদি XML UI)
└── di/
└── AppModule.kt # Hilt/Koin ডিপেন্ডেন্সি ইনজেকশন
১. Gradle Dependencies (build.gradle.kts)
dependencies {
// Core
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// UI - Compose (Recommended)
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// UI - XML Alternative
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.google.android.material:material:1.11.0")
// DI - Hilt (Optional)
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
}
২. ডেটা মডেল (WifiNetwork.kt)
package com.example.wifiscannerpro.data.model
import android.net.wifi.ScanResult
data class WifiNetwork(
val ssid: String,
val bssid: String,
val security: SecurityType,
val signalLevelDbm: Int, // -20 (Excellent) to -100 (Unusable)
val frequencyMHz: Int, // 2412, 2437, 5180, etc.
val channelWidth: Int, // 20, 40, 80, 160 MHz
val is5GHz: Boolean,
val timestamp: Long = System.currentTimeMillis()
) {
companion object {
fun fromScanResult(result: ScanResult): WifiNetwork {
return WifiNetwork(
ssid = result.SSID,
bssid = result.BSSID,
security = SecurityType.fromCapabilities(result.capabilities),
signalLevelDbm = result.level,
frequencyMHz = result.frequency,
channelWidth = result.channelWidth,
is5GHz = result.frequency > 4000 // 5GHz band > 4000 MHz
)
}
}
}
enum class SecurityType(val displayName: String) {
WPA3("WPA3"), WPA2("WPA2"), WPA("WPA"), WEP("WEP"), OPEN("Open"), UNKNOWN("Unknown");
companion object {
fun fromCapabilities(capabilities: String): SecurityType {
return when {
capabilities.contains("WPA3", true) -> WPA3
capabilities.contains("WPA2", true) -> WPA2
capabilities.contains("WPA", true) -> WPA
capabilities.contains("WEP", true) -> WEP
capabilities.contains("ESS", true) -> OPEN // Open but ESS
else -> UNKNOWN
}
}
}
}
৩. রিপোজিটরি (WifiRepository.kt)
package com.example.wifiscannerpro.data
import android.content.Context
import android.net.wifi.ScanResult
import android.net.wifi.WifiManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.wifiscannerpro.data.model.WifiNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asLiveData
class WifiRepository(private val context: Context) {
private val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
private val scanResultsFlow = MutableStateFlow>(emptyList())
private val scanStateFlow = MutableStateFlow(ScanState.Idle)
private var scanReceiver: WifiScanReceiver? = null
// UI এক্সপোজ করা LiveData/Flow
val networks: LiveData> = scanResultsFlow.asLiveData()
val scanState: LiveData = scanStateFlow.asLiveData()
sealed interface ScanState {
data class Scanning(val progress: String) : ScanState
data class Success(val count: Int) : ScanState
data class Error(val message: String) : ScanState
object Idle : ScanState
}
fun startScan(permissionGranted: Boolean) {
if (!permissionGranted) {
scanStateFlow.value = ScanState.Error("Location/WiFi permission required")
return
}
if (!wifiManager.isWifiEnabled) {
scanStateFlow.value = ScanState.Error("WiFi is disabled")
return
}
scanStateFlow.value = ScanState.Scanning("Scanning for networks...")
// Register receiver
scanReceiver = WifiScanReceiver()
val filter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(scanReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
// Trigger scan
wifiManager.startScan()
}
inner class WifiScanReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION == intent.action) {
context.unregisterReceiver(this)
scanReceiver = null
val results = wifiManager.scanResults
val networks = processResults(results)
scanResultsFlow.value = networks
scanStateFlow.value = ScanState.Success(networks.size)
}
}
}
private fun processResults(results: List): List {
val seenBssids = mutableSetOf()
val networks = mutableListOf()
for (result in results) {
if (seenBssids.add(result.BSSID)) { // Unique BSSID only
networks.add(WifiNetwork.fromScanResult(result))
}
}
// Sort by signal strength descending
networks.sortByDescending { it.signalLevelDbm }
return networks
}
fun cleanup() {
scanReceiver?.let { context.unregisterReceiver(it) }
scanReceiver = null
}
}
৪. ViewModel (MainViewModel.kt)
package com.example.wifiscannerpro.ui
import android.Manifest
import android.content.pm.PackageManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.wifiscannerpro.data.WifiRepository
import com.example.wifiscannerpro.data.model.WifiNetwork
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import androidx.core.content.ContextCompat
class MainViewModel(private val repository: WifiRepository) : ViewModel() {
// UI State
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asLiveData()
data class UiState(
val networks: List = emptyList(),
val scanState: WifiRepository.ScanState = WifiRepository.ScanState.Idle,
val hasPermission: Boolean = false
)
init {
checkPermissions()
}
private fun checkPermissions() {
// Simplified - real app would use ActivityResultContracts
val context = repository.context
val hasLocationPerm = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED
val hasNearbyPerm = android.os.Build.VERSION.SDK_INT < 33 ||
ContextCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES)
== PackageManager.PERMISSION_GRANTED
_uiState.update { it.copy(hasPermission = hasLocationPerm && hasNearbyPerm) }
}
fun onScanClick() {
_uiState.update { it.copy(scanState = WifiRepository.ScanState.Scanning("Starting scan...")) }
repository.startScan(_uiState.value.hasPermission)
}
fun onNetworkClick(network: WifiNetwork) {
// Handle connection logic here
}
}
৫. Compose UI (MainActivity.kt - Modern Approach)
package com.example.wifiscannerpro.ui
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.wifiscannerpro.data.model.WifiNetwork
import com.example.wifiscannerpro.data.model.SecurityType
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
viewModel.checkPermissions() // Update state
}
private val viewModel: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WiFiScannerScreen(
viewModel = viewModel,
onPermissionRequest = { requestPermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.NEARBY_WIFI_DEVICES
)) }
)
}
}
}
}
}
@Composable
fun WiFiScannerScreen(
viewModel: MainViewModel,
onPermissionRequest: () -> Unit
) {
val uiState by viewModel.uiState.observeAsState()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Header
Text("WiFi Scanner Pro", fontSize = 24.sp, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineMedium)
// Scan Button
Button(
onClick = { viewModel.onScanClick() },
enabled = uiState.hasPermission && uiState.scanState !is WifiRepository.ScanState.Scanning,
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (uiState.hasPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHighest
)
) {
when (uiState.scanState) {
is WifiRepository.ScanState.Scanning -> {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary)
Spacer(Modifier.width(12.dp))
Text("Scanning...", color = MaterialTheme.colorScheme.onPrimary)
}
}
else -> Text(if (uiState.hasPermission) "Scan WiFi Networks" else "Grant Permissions First", color = MaterialTheme.colorScheme.onPrimary)
}
}
// Permission Warning
if (!uiState.hasPermission) {
Text(
"Location & Nearby WiFi permissions required for scanning",
color = MaterialTheme.colorScheme.error,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp)
)
Button(onClick = onPermissionRequest, modifier = Modifier.padding(16.dp)) {
Text("Grant Permissions")
}
}
// Network List
if (uiState.networks.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.networks) { network ->
WifiNetworkCard(network = network)
}
}
} else if (uiState.scanState is WifiRepository.ScanState.Success) {
Text("No networks found", color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.CenterHorizontally))
} else if (uiState.scanState is WifiRepository.ScanState.Error) {
Text((uiState.scanState as WifiRepository.ScanState.Error).message, color = MaterialTheme.colorScheme.error, modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.CenterHorizontally))
}
}
}
@Composable
fun WifiNetworkCard(network: WifiNetwork) {
val signalColor = when {
network.signalLevelDbm >= -50 -> Color.Green
network.signalLevelDbm >= -70 -> Color(0xFFFFC107) // Amber
else -> Color.Red
}
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = if (network.ssid.isBlank()) "" else network.ssid,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${network.signalLevelDbm} dBm",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = signalColor
)
}
Spacer(Modifier.height(8.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Chip(label = { Text(network.security.displayName, fontSize = 12.sp) }, colors = ChipDefaults.chipColors(containerColor = MaterialTheme.colorScheme.primaryContainer))
Chip(label = { Text(if (network.is5GHz) "5 GHz" else "2.4 GHz", fontSize = 12.sp) }, colors = ChipDefaults.chipColors(containerColor = MaterialTheme.colorScheme.secondaryContainer))
Chip(label = { Text("Ch: ${frequencyToChannel(network.frequencyMHz)}", fontSize = 12.sp) })
}
}
}
}
fun frequencyToChannel(freq: Int): Int = when {
freq >= 2412 && freq <= 2484 -> (freq - 2412) / 5 + 1
freq >= 5170 && freq <= 5825 -> (freq - 5000) / 5
else -> 0
}
৬. XML UI Alternative (MainActivityXML.kt)
যদি XML পছন্দ করেন, তবে পূর্বের আর্টিকেলের (1006061518) `RecyclerView` + `WifiNetworkAdapter` প্যাটার্নটি ব্যবহার করুন, শুধু `MainActivity`-এ `ViewModel` থেকে `LiveData` observe করুন:
// MainActivity.java (XML version)
viewModel.networks.observe(this, networks -> adapter.submitList(networks));
viewModel.scanState.observe(this, state -> {
// Update UI based on state (loading, error, success)
});
btnScan.setOnClickListener(v -> viewModel.onScanClick());
৭. পারমিশন হ্যান্ডলিং (ActivityResultContracts - Modern Way)
// Activity-এ
private val requestPermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
val allGranted = perms.values.all { it }
viewModel.onPermissionResult(allGranted)
}
// Scan বাটনে ক্লিকে
if (!viewModel.hasPermission) {
requestPermissions.launch(arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.NEARBY_WIFI_DEVICES
))
} else {
viewModel.onScanClick()
}
৮. প্রোডাকশন রেডি চেকলিস্ট
- [x] MVVM Architecture
- [x] Coroutines + Flow (Reactive)
- [x] Modern Permissions (API 33+ NEARBY_WIFI_DEVICES)
- [x] Scan Throttling Awareness (API 29+)
- [x] Duplicate BSSID Filtering
- [x] Signal Strength Sorting
- [x] Material3 / Compose UI
- [x] Hidden Network Handling
- [x] Channel/Frequency Info
- [ ] Unit Tests (Repository, ViewModel)
- [ ] UI Tests (Compose Testing)
- [ ] Accessibility (Content Descriptions)
- [ ] Offline Caching (Room)
- [ ] Background Scanning (WorkManager)
সারাংশ
এই গাইডটি একটি প্রোফেশনাল, মেইনটেনেবল, এবং স্কেলেবল ওয়াইফাই স্ক্যানার অ্যাপ বানানোর পূর্ণাঙ্গ ব্লুপ্রিন্ট দিয়েছে। MVVM আর্কিটেকচার, Kotlin Coroutines/Flow, এবং Jetpack Compose ব্যবহার করে এটি মডার্ন অ্যান্ড্রয়েড ডেভেলপমেন্টের সেরা অনুশীলন অনুসরণ করে। XML-ভিত্তিক লিগ্যাসি কোডবেসের জন্য RecyclerView + ViewModel প্যাটার্নটি ব্যবহার করুন।