কম্পিউটার টিউটোরিয়াল

অ্যান্ড্রয়েডে ওয়াইফাই স্ক্যানার অ্যাপ ডেভেলপ করার সম্পূর্ণ গাইড

এই আর্টিকেলটি একটি প্রোডাকশন-লেভেল 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 প্যাটার্নটি ব্যবহার করুন।

  1. অ্যান্ড্রয়েডের জন্য টপ ৫ QR স্ক্যানার অ্যাপ

  2. ওয়াইফাই দিয়ে ফাইল ট্রান্সফারের ৪টি সেরা অ্যান্ড্রয়েড অ্যাপ

  3. অ্যান্ড্রয়েডের ৯টি সেরা ডকুমেন্ট স্ক্যানার অ্যাপ — ক্যাপচার, এডিট ও PDF কনভার্ট

  4. পিসি জন্য টপ ১২ অ্যান্ড্রয়েড অপারেটিং সিস্টেম: পারফরম্যান্স ও অ্যাপ অ্যাক্সেস বাড়ান