بررسی وضعیت اتصال اینترنت در اندروید Q – از صفر تا صد


اگر صرفاً با منظور آزمایش تاکنون API سطح 29 را در پروژههای اندروید تارگت کرده باشید، در خوشبینانهترین حالت صرفاً با برخی هشدارها مواجه میشوید که در فرایند بیلد ظاهر میشوند و در نهایت بیلد با موفقیت به پایان میرسد. اما در صورتی که فکر میکنید هیچ مشکلی وجود نخواهد داشت، ممکن است در مورد برخی کلاسهای خاص مانند NetworkInfo کاملاً شگفتزده شوید. در این مقاله در مورد روشهای بررسی وضعیت اتصال اینترنت در اندروید Q صحبت خواهیم کرد.
چرا باید اندروید Q را تارگت کنیم؟
اندروید 10 یا اندروید Q به طور رسمی در تاریخ 3 سپتامبر 2019 (12 شهریور 1398) منتشر شده است و نسخه بعد از اندروید 9 و جدیدترین نسخه اندروید محسوب میشود که از سنت نامگذاری بر اساس نام شیرینیها پیروی نمیکند.
اغلب توصیه میشود که هر چند برای تست آخرین نسخه اندروید را تارگت کنیم و گوگل پلی استور نیز در این خصوص پیشنهادهایی به شرح زیر دارد:
همان طور که میبینید هر سال الزام گوگلی پلی به نسخه جدید ارتقا مییابد. این بدان معنی است که از تاریخ 1 آگوست 2020 (11 مرداد 1399) اپلیکیشنهای جدید باید API سطح 29 را تارگت کنند. بنابراین زمان زیادی برای تست این که همه چیز روی نسخه جدید به درستی کار میکند ندارید.
قبل از هر چیز باید نسخه جدید را تارگت کنیم تا ببینیم چه اتفاقاتی رخ میدهند. به این منظور کافی است گزاره targetSdkVersion را در پیکربندی پیدا کنید و آن را روی 29 تنظیم کنید. سپس پروژه را کامپایل کنید و هشدارها و خطاهای کامپایل را بررسی کنید.
در ادامه نمونهای از هشدارهایی را که ممکن است بگیرید ارائه کردهایم. این هشدارها از پروژه واقعی که سطح 28 به 29 ارتقا یافته ناشی شدهاند:
Type mismatch: inferred type is MenuItem? but MenuItem was expected Type mismatch: inferred type is Configuration? but Configuration was expected Type mismatch: inferred type is String? but String was expected 'setColorFilter(Int, PorterDuff.Mode): Unit' is deprecated. Deprecated in Java Type mismatch: inferred type is Date? but Date was expected Unsafe use of a nullable receiver of type Any? 'PreferenceManager' is deprecated. Deprecated in Java 'NetworkInfo' is deprecated. Deprecated in Java 'getter for activeNetworkInfo: NetworkInfo!' is deprecated. Deprecated in Java 'getter for isConnectedOrConnecting: Boolean' is deprecated. Deprecated in Java
در لیست فوق به جز بررسیهای null و اضافه شدن پکیج androidx.preference چیز زیادی وجود ندارد. همه این موارد در طی 20 دقیقه کاملاً رفع میشوند. اما در ادامه با مشکل منسوخ شدن NetworkInfo مواجه میشویم.
NetworkInfo در سطح 29 منسوخ شده است:
فراخوانیکنندهها به جای ConnectivityManager.NetworkCallback باید با تغییراتی که در زمینه اتصالپذیری واقع شده آشنا شوند و از ConnectivityManager#getNetworkCapabilities یا ConnectivityManager#getLinkProperties برای دریافت ناهمگام اطلاعات استفاده کنند.
یک روش برای حل این مشکل این است که به راهنماییهای ارتقای رسمی (+) روی وبسایت اندروید نگاه کنیم. متأسفانه در این وبسایت اطلاعات زیادی ارائه نشده است و از این رو باید از قدرت ماهیچههای ذهن برای حل این مشکل کمک بگیریم. پیش از این اخبارِ بد در مورد حالت اتصالپذیری تنها کاری که باید انجام میدادیم به صورت زیر بود:
1class NetworkUtils(private val context: Context) {
2 fun isConnected(): Boolean {
3 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
4 val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
5 var result = false
6 if (activeNetwork != null) {
7 result = activeNetwork.isConnectedOrConnecting
8 }
9 return result
10 }
11}
این کد باید از هر جایی با فرض دسترسی به Context اپلیکیشن فراخوانی میشود و به راحتی میشد بررسی کرد که آیا دستگاه در حال حاضر به شبکه دسترسی دارد یا نه. برای دریافت تغییرات در اتصال دستگاه به شبکه نیز باید به صورت زیر عمل میشد:
- فایل BaseActivity.kt
1abstract class BaseActivity : AppCompatActivity() {
2 private var networkCallback: ConnectivityManager.NetworkCallback? = null
3 private val networkUtils by lazy { NetworkUtils(applicationContext) /*important to be applicationContext to prevent memoryLeak*/}
4 private val connectivityManager by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
5
6 protected val NetworkStateReceiverListener.isConnected: Boolean // can be used by childs
7 get() {
8 this as BaseActivity // only accessible from child class, so cast i safe here
9 return networkUtils.isConnected()
10 }
11
12 private fun registerConnectivityMonitoring(listener: NetworkStateReceiverListener) {
13 val networkCallback = object : ConnectivityManager.NetworkCallback() {
14 override fun onAvailable(network: Network?) {
15 listener.networkConnectivityChanged()
16 }
17
18 override fun onLost(network: Network?) {
19 listener.networkConnectivityChanged()
20 }
21 }
22 this.networkCallback = networkCallback
23 connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), networkCallback)
24 }
25
26 private fun unregisterConnectivityMonitoring() {
27 val networkCallback = this.networkCallback ?: return
28 connectivityManager.unregisterNetworkCallback(networkCallback)
29 this.networkCallback = null
30 }
31
32 override fun onCreate(savedInstanceState: Bundle?) {
33 super.onCreate(savedInstanceState)
34 if (this is NetworkStateReceiverListener)
35 registerConnectivityMonitoring(this)
36 }
37
38 override fun onDestroy() {
39 super.onDestroy()
40 if (this is NetworkStateReceiverListener)
41 unregisterConnectivityMonitoring()
42 }
43
44 override fun onResume() {
45 super.onResume()
46 if (this is NetworkStateReceiverListener && !isConnected)
47 this.networkConnectivityChanged()// call to show no network banner on activity resume
48 }
49}
- فایل MyActivity.kt
1class MainActivity : BaseActivity(), NetworkStateReceiverListener {
2 override fun networkConnectivityChanged() {
3 if (isConnected) {
4 //show that that the network is back
5 } else {
6 //show that that the network was lost
7 }
8 }
9}
- فایل NetworkStateReceiverListener.kt
1interface NetworkStateReceiverListener {
2 fun networkConnectivityChanged() {}
3}
اکنون که ConnectivityManager#getActiveNetworkInfo را از دست دادهایم تنها روش برای بررسی ناهمگام اتصال دستگاه به شبکه استفاده از API زیر است:
- NetworkCallback – این API است که از قبل میشناسیم و شاید حتی استفاده کردهایم. این یک اینترفیس برای پیادهسازی دریافت رویدادهای اتصالپذیری دستگاه است.
- ConnectivityManager#getNetworkCapabilities – با توجه به شبکه، ظرفیتهایی مانند نوع شبکه (موبایل یا وای فای) پهنای باند و غیره را میگیرد.
- ConnectivityManager#getLinkProperties – با توجه به شبکه مشخصههای لینک شبکه مانند اینترفیس شبکه را عرضه میکند.
در لیست فوق موارد 2 و 3 از API سطح 21 وجود داشتهاند، اما تاکنون زیاد با آنها کار نکردهایم و به نظر میرسد که نتیجهای مشابه کد قبلی به دست نمیدهد.
هر دو آنها به یک آرگومان Network نیاز دارند که با ConnectivityManager#getActiveNetwork به دست میآید، اما در نهایت هیچ کدام معادل NetworkInfo#isConnected یا NetworkInfo#isConnectedOrConencting نیستند.
چاره کار این است که حالت اتصال را جایی در اندروید ذخیره کنیم. به این منظور باید برخی موارد را در پیادهسازی NetworkCallback مورد بازنگری قرار دهیم.
جایگزینی برای NetworkInfo
کار خود را با ایجاد یک جایگزین NetworkInfo آغاز میکنیم که یک روش ناهمگام برای دریافت حالت اتصال شبکه ایجاد میکند. در ادامه مثالی از اینترفیس آن را میبینید:
1interface NetworkState {
2 val isConnected: Boolean
3 val network: Network?
4 val networkCapabilities: NetworkCapabilities?
5 val linkProperties: LinkProperties?
6}
اکنون باید این اینترفیس را پیادهسازی کنیم و مطمئن شویم که مقادیر آن اتصالپذیری دستگاه را بازتاب میدهند:
1internal class NetworkStateImp : NetworkState {
2 override var isConnected: Boolean = false
3 override var network: Network? = null
4 override var linkProperties: LinkProperties? = null
5 override var networkCapabilities: NetworkCapabilities? = null
6}
اکنون میتوانیم از این پیادهسازی درون NetworkCallback استفاده کنیم و حالت شبکه را درون آن ذخیره کرده و هر بار که حالت تغییر مییابد بهروزرسانی کنیم:
1internal class NetworkCallbackImp(private val holder: NetworkStateImp) : ConnectivityManager.NetworkCallback() {
2 override fun onAvailable(network: Network) {
3 holder.network = true
4 holder.isConnected = isAvailable
5 }
6 override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
7 holder.networkCapabilities = networkCapabilities
8 }
9 override fun onLost(network: Network) {
10 holder.network = network
11 holder.isConnected = false
12 }
13 override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
14 holder.linkProperties = linkProperties
15 }
16 }
بدین ترتیب موفق شدیم مشکل را حل کنیم، اما هنوز مواردی هستند که باید تغییر دهیم:
- نخستین مورد این است که باید یک وهله از NetworkState ایجاد کنیم که از هر جایی در اپلیکیشن و نه فقط NetworkStateImp قابل دسترسی باشد. به این منظور یک شیء فقط-خواندنی نیاز داریم.
- مورد دوم شیوه اطلاعرسانی به هر یک از طرفین ذینفع در مورد تغییر حالت اتصالپذیری شبکه است.
- در نهایت مورد سوم اتصال همه این موارد به NetworkStateImp است.
نکته دوم و سوم چندین راهحل دارند. برخی ممکن است از یک کانتینر DI استفاده کنند که از قبل برای اپلیکیشن تنظیم کردهاند، اما اگر بخواهیم همه چیز ساده باشد، میتوانیم از کد کاتلین زیر استفاده کنیم:
1object NetworkStateHolder : NetworkState {
2
3 private lateinit var holder: NetworkStateImp
4
5 override val isConnected: Boolean
6 get() = holder.isConnected
7 override val network: Network?
8 get() = holder.network
9 override val networkCapabilities: NetworkCapabilities?
10 get() = holder.networkCapabilities
11 override val linkProperties: LinkProperties?
12 get() = holder.linkProperties
13
14 fun Application.registerConnectivityMonitor() {
15 holder = NetworkStateImp()
16 val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
17 connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), NetworkCallbackImp(holder))
18 }
19
20}
در کد فوق کارهای زیر را انجام میدهیم:
- Object یک syntactic sugar کاتلین برای تنظیم الگوی سینگلتون محسوب میشود.
- NetworkStateHolder یک وهله از NetworkState است، اما مقادیر در مشخصه holder ذخیره شدهاند. Holder یک NetworkStateImp و قابل ویرایش، اما خصوصی است و نکته اول فوق را برآورده میسازد.
- registerConnectivityBroadcaster یک اکستنشن Application است که holder را به callback اتصال میدهد و نکته سوم فوق را برآورده میسازد.
برای راهاندازی همه معماری نظارتی کافی است registerConnectivityBroadcaster را به صورت زیر فراخوانی کنیم:
1class MainApplication : Application() {
2 override fun onCreate() {
3 super.onCreate()
4 registerConnectivityBroadcaster()
5 }
6}
اکنون تنها نکته دوم فوق باقی مانده است یعنی تغییرات شبکه را منتشر کنیم. یک روش قدیمی در این مورد میتواند استفاده از نوعی الگوی Broadcaster -> Intent -> Receiver -> Function برای دریافت رویدادها در اکتیویتیها باشد.
اما قرار نیست همیشه در گذشته بمانیم و باید شروع به استفاده از راهکارهای جت پک اندروید بکنیم. به این منظور میخواهیم از کامپوننتهای معماری بهره بگیریم.
استفاده از نوعی LiveData
ما با استفاده از LiveData نکته فوق فهرست مشکلات فوق را که پیشتر ارائه کردیم حل میکنیم، یعنی رویدادهای اتصالپذیری را به بقیه بخشهای اپلیکیشن اطلاعرسانی میکنیم. ابتدا باید برخی رویدادها را تعریف کنیم. در این مورد نیز از کد کاتلین استفاده میکنیم:
1sealed class Event {
2
3 val networkState: NetworkState = NetworkStateHolder
4
5 object ConnectivityLost : Event()
6 object ConnectivityAvailable : Event()
7 data class NetworkCapabilityChanged(val old: NetworkCapabilities?) : Event()
8 data class LinkPropertyChanged(val old: LinkProperties?) : Event()
9}
در کد ساده فوق چند الگو تعریف شدهاند:
- Enumeration – کلاس sealed امکان مجموعه محدودی از انواع مبتنی بر Event را فراهم میسازد که همه آنها درون event قرار دارند و خواندشان آسان است.
- Polymorphism – هر وهله از Event یک حالت شبکه را نگهداری میکند، اما برخی دادههای خاص مرتبط با تغییر رخ داده نیز ذخیره کرده است.
- Abstraction – اگر هیچ دادههای لازم نباشد، میتوانیم صرفاً از نوع برای مدیریت رویداد استفاده کنیم که دقیقاً مشابه مدیریت likeException است. این حالت در مورد ConnectivityLost و ConnectivityAvailable مصداق دارد. همچنین برای سادگی بیشتر استفاده از object/singletons استفاده شده است.
به فایلهای زیر توجه کنید:
- فایل NetworkEvents.kt
1object NetworkEvents : LiveData<Event>() {
2 internal fun notify(event: Event) {
3 postValue(event)
4 }
5}
- فایل NetworkStateImp.kt
1internal class NetworkStateImp : NetworkState {
2 override var network: Network? = null
3
4 override var isConnected: Boolean = false
5 set(value) {
6 field = value
7 NetworkEvents.notify(if (value) Event.ConnectivityAvailable else Event.ConnectivityLost)
8 }
9 override var linkProperties: LinkProperties? = null
10 set(value) {
11 val event = Event.LinkPropertyChanged(field)
12 field = value
13 NetworkEvents.notify(event)
14 }
15
16 override var networkCapabilities: NetworkCapabilities? = null
17 set(value) {
18 val event = Event.NetworkCapabilityChanged(field)
19 field = value
20 NetworkEvents.notify(event)
21 }
22}
اکنون موارد زیر را داریم:
NetworkEvents یک LiveData است و با استفاده از NetworkEvents.observe(lifecycleowner, observer) میتوان آن را از هر LifeCycleOwner مشاهده کرد. همچنین میتوان از هر مکانی که دارای NetworkEvents.observeForever(observer) باشد آن را مشاهده کرد.
Notify تابعی است که برای ارسال رویدادهای جدید استفاده میشود. در NetworkHolderImp که قبلاً ارائه کردیم با افزودن یک setter که NetworkEvents.notify را فراخوانی میکند، میتوانیم هر چیزی که تغییر یافته را به اطلاع همه observer-ها برسانیم.
سخن پایانی
احتمالاً در این مقاله برخی کلیدواژههای internal را در ابتدای کلاسها و مشخصهها دیدید. منظور از آن این است که تنها از درون ماژول قابل مشاهده هستند. به بیان خلاصه کد را در ماژول خاص خود قرار میدهیم و نمایانی آن را به کلاسهای خودمان محدود میکنیم. برای مشاهده کد کامل این پروژه میتوانید به این رپیوی گیتهاب (+) بروید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای پروژهمحور برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی اندروید (Android) – مقدماتی
- نکات کلیدی اندروید ۱۰ برای توسعه دهندگان — راهنمای کاربردی
- محدود و مسدودسازی اپلیکیشن ها در اندروید— به زبان ساده
==