إنشاء مشروع بإستخدام Retrofit و MvvM و Hilt و coroutines في Kotlin Compose

أهمية استخدام Retrofit و MVVM و Hilt و Coroutines معًا

إنشاء مشروع بإستخدام Retrofit و MvvM و Hilt و coroutines في Kotlin Compose

في هذه المقالة، سنتعلم كيفية إنشاء مشروع كامل باستخدام عدة مكتبات مثل Retrofit لإدارة واجهة برمجة التطبيقات (API)، وMVVM كهندسة لتنظيم التعليمات البرمجية، وHilt لإدارة التبعيات، وCoroutines للتعامل مع العمليات غير المتزامنة. سنقوم ببناء المشروع خطوة بخطو. ستساعدك هذه المقالة على فهم كيفية استخدام هذه المكتبات معًا لإنشاء تطبيق فعال وقابل للتطوير.


خطوات إنشاء المشروع:

1. إنشاء وحدات

الخطوة الأولى هي إنشاء وحدات في المشروع باستخدام واجهة Android Studio. لنبدأ بالنقر بزر الماوس الأيمن على المشروع واختيار "وحدة جديدة"، ثم اختيار مكتبة Android وتسميتها "utils".

2. إضافة الخدمات

داخل وحدة utils، نضع خدمات مثل التحكم في اتصال الإنترنت أو حالات استجابة واجهة برمجة التطبيقات (API). على سبيل المثال:

اولا سوف نحتاج لعمل modules عن طريق Right Click ومن ثم اختيار Moduls


إضافة التبعيات اللازمة (Retrofit, Hilt, Coroutines, Jetpack Compose).

بعدها اختر من القائمه Android Library وضغ اسم لها وهنا قمت بعمل utils


حقن الاعتماديات في ViewModel و Retrofit

سوف نضع فيها utils الخاصه بالتطبيق مثل التاكد من الانترنت وحالات التطبيق الخاص بنا خلال عملية response

object CoreUtility {

    fun isInternetConnection(context: Context): Boolean {
        val connectionManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
        val networkCapabilities = connectionManager.activeNetwork ?: return false
        val actNw = connectionManager.getNetworkCapabilities(networkCapabilities) ?: return false
        val result = when {
            actNw.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> true
            actNw.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> true
            else -> false
        }
        return result
    }

}

sealed class ResourceState<T> {
    data class Success<T>(val data: T): ResourceState<T>()
    data class Error<T>(val error: String): ResourceState<T>()
    class Loading<T>: ResourceState<T>()
}
انتقل الى BuildGradl.app
واضف الامتداد التالي ليقرأ الملف الذي قمنا بإنشاءه وايضا المكتبات التي سوف نحتاجها
implementation(project(":utils"))
    implementation(libs.androidx.constraintlayout.compose)
    implementation(libs.androidx.lifecycle.viewmodel.ktx)
    implementation(libs.androidx.navigation.compose)
    implementation(libs.androidx.hilt.navigation.compose)
    implementation(libs.hilt.android)
    kapt(libs.hilt.compiler)
    implementation (libs.retrofit)
    implementation (libs.converter.gson)
    implementation (libs.okhttp)
    implementation(libs.moshi.kotlin)
    implementation (libs.converter.moshi)
    implementation (libs.logging.interceptor)
    implementation (libs.kotlinx.coroutines.android)
    implementation (libs.kotlinx.coroutines.core)
    implementation(libs.coil.compose)
    
الان انتقل الى libs.versions.toml
سوف نضع هنا اصدارات المكاتب وايضا الاكواد الخاصه بعملية التثبيت
[versions]
lifecycleViewmodelKtx = "2.8.5"
moshiKotlin = "1.12.0"
kotlinxCoroutinesAndroid = "1.7.3"
navigationCompose = "2.8.0"
hiltCompiler = "2.51.1"
hiltNavigationCompose = "1.0.0"
constraintlayoutCompose = "1.0.1"
okhttp = "5.0.0-alpha.2"
retrofit = "2.9.0"
converterGson = "2.9.0"
coilCompose = "2.7.0"

[libraries]
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltCompiler" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesAndroid" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

التعامل مع apis باستخدام retrofit و kotkin compose

سوف نقوم اولا بعمل class يحتوي على النصوص
object ApiStrings {
    const val BASE_URL = "https://newsapi.org/"
    const val API_KEY = "f513ae11635c4d4697f83a2adead9cbe"
    const val Language = "ar"

//    https://newsapi.org/v2/top-headlines?country=us&apiKey=f513ae11635c4d4697f83a2adead9cbe

}

انشاء ApiService للتعامل مع API

interface ApiService {

    @GET("v2/top-headlines")
    suspend fun getNews(@Query("country") country: String, @Query("apiKey") apiKey: String = ApiStrings.API_KEY) : Response<NewsResponse>
}

تهيئة Retrofit

سنقوم بتهيئة Retrofit لربط التطبيق بالـ API باستخدام Hilt:
object Routes {
    const val HomeScreen = "Home"
}

@Composable
fun AppNavHost(
    navController: NavHostController,
    startDestination: String = Routes.HomeScreen,
) {
    NavHost(
        navController = navController, startDestination = Routes.HomeScreen
    ) {
        composable(Routes.HomeScreen,) { HomeNews(navController = navController) }
        composable(
            route = "${NavigationItem.Details.route}/{name}",
            arguments = listOf(navArgument("name") { type = NavType.StringType })        )
        { SecondScreen(navController) }
    }
}
تهيئة ملف ProvideRetrofit لإنشاء Api وتهيئة محتويات ملفات clean architecture
@Module
@InstallIn(SingletonComponent::class)
class ProvideRetrofit {

    @Provides
    @Singleton
    fun provideRetrofit() : Retrofit {

        val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        val httpClint = okhttp3.OkHttpClient.Builder().apply {
            addInterceptor(httpLoggingInterceptor)
        }

        httpClint.apply {
            readTimeout(60,TimeUnit.SECONDS)
        }

        val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()

        return Retrofit.Builder()
            .baseUrl(ApiStrings.BASE_URL)
            .client(httpClint.build())
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
    }

    @Provides
    @Singleton
    fun providesApiService(retrofit: Retrofit) : ApiService = retrofit.create(ApiService::class.java)

    @Provides
    @Singleton
    fun providesRemoteDataSource(apiService: ApiService) : NewsRepo = NewsDataSource(apiService)

    @Provides
    @Singleton
    fun providesNewsRepository(
        dataSource: NewsDataSource,
        @ApplicationContext context: Context
    ): NewsRepoImp {
        return NewsRepoImp(dataSource, context)
    }

} 

إنشاء Clean Architecture لمشروعك

لتنظيم المشروع وفق العمارة النظيفة، سنقوم بإنشاء ثلاث طبقات رئيسية:

يتضمن 3 عناصر وهيا Domain , Data , presentation

محتوى Domain

يحتوي على واجهة repository
interface NewsRepo {
    suspend fun getNews(country:String) : Response<NewsResponse>
}

محتويات Data

سوف نقوم بوضع model الذي سوف نعمل عليه
data class NewsResponse(
    val status: String,
    val totalResults: Int,
    val articles: List<Articles>,
)

data class Articles(
    val author: String?,
    val title: String,
    val description: String?,
    val url: String?,
    val urlToImage: String?,
)

محتوى DataSource

يحتوي على مصدر البيانات
 
class NewsDataSource @Inject constructor(
    private val apiService: ApiService
) : NewsRepo {

    override suspend fun getNews(country: String): Response<NewsResponse> {
        return apiService.getNews(country)
    }

}
هذا المسؤول عن عملية ارسال البيانات الى dataSource للتعامل معها Repository
class NewsRepoImp @Inject constructor(
    private val dataSource: NewsDataSource,
    private val context: Context
) {

     suspend fun getNews(country: String): Flow<ResourceState<NewsResponse>> {
        return flow {
            emit(ResourceState.Loading())

            if (!CoreUtility.isInternetConnection(context)) {
                emit(ResourceState.Error(error = "No internet connection"))
                return@flow
            }

            val response = dataSource.getNews(country)

            if (response.isSuccessful && response.body() != null) {
                emit(ResourceState.Success(response.body()!!))
            } else {
                emit(ResourceState.Error(error = response.message() ?: "Error Fetching Data"))
            }
        }.catch { e->
            emit(ResourceState.Error(error = e.localizedMessage ?: "Error Fetching Data"))

        }
    }

}

محتويات presentation

سوف نحتاج للتعامل مع البيانات واستدعائها manager وهذا يكون ViewModel
سنقوم هنا بمعالجة البيانات باستخدام ViewModel:
@HiltViewModel
class NewsViewModel @Inject constructor(
    private val repository: NewsRepoImp
): ViewModel() {

    private val _news : MutableStateFlow<ResourceState<NewsResponse>> = MutableStateFlow(ResourceState.Loading())
    val news : StateFlow<ResourceState<NewsResponse>> = _news

    init {
        Log.i(TAG, "Running: ------ ")
        getData()
    }

    private fun getData() {
        viewModelScope.launch(Dispatchers.IO) { repository.getNews("us").collectLatest { response ->
            _news.value = response
            Log.i(TAG, "Running: ------ loaded")
            Log.i(TAG, "Running: ------ ${_news.value}")

        } }
    }

    companion object {
        private const val TAG = "NewsViewModel"
    }

}

إدارة واجهات برمجة التطبيقات (APIs) باستخدام Retrofit

نقوم هنا بتوصيل واجهة المستخدم بالبيانات من خلال ViewModel
const val TAG = "Home_Screen"

@Composable
fun HomeNews(navController: NavHostController, viewModel: NewsViewModel = hiltViewModel()) {
    val newsResponse = viewModel.news.collectAsState()
    val response = newsResponse.value

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {}
    ) { innerPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
                contentAlignment = Alignment.Center
        ) {
            when (response) {
                is ResourceState.Loading -> {
                    CircularProgressIndicator(
                        modifier = Modifier.size(50.dp)
                    )
                }

                is ResourceState.Success -> {
                    Log.i(TAG, "HomeNews: Success")
                    val newsData = response.data
                    LazyColumn {
                        items(newsData.articles) { article ->
                            Column {
                                AsyncImage(
                                    model = article.urlToImage,
                                    contentDescription = null,
                                    contentScale = ContentScale.Crop,
                                    error = painterResource(id = R.drawable.img2),
                                    placeholder = painterResource(id = R.drawable.img2),
                                    modifier = Modifier.padding(15.dp).clip(RoundedCornerShape(15.dp)).height(240.dp).fillMaxWidth()
                                )
                                Spacer(modifier = Modifier.height(10.dp))
                                Text(text = article.title)
                                Text(text = "*".repeat(20))
                                Spacer(modifier = Modifier.height(20.dp))

                            }
                        }
                    }
                }

                is ResourceState.Error -> {
                    Text(text = "Error: ${response.error}")
                    Toast.makeText(
                        navController.context,
                        response.error,
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }
}

تهيئة صفحة main لتشغيل التطبيق

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppNavHost(
                navController = rememberNavController(),
            )
        }
    }
}

ما هي مزايا استخدام Hilt على نظام Android؟

يساعد Hilt على تقليل تعقيد إدارة التبعيات عن طريق حقنها مباشرة في مكونات التطبيق باستخدام مكتبة Dagger، مما يسهل العمل على المشاريع الكبيرة.

لماذا نستخدم Retrofit  لإدارة واجهات برمجة التطبيقات؟

يوفر Retrofit  طريقة مرنة وفعالة للعمل مع واجهات برمجة التطبيقات RESTful ويسمح لك بمعالجة استجابات JSON بسهولة وتحويلها إلى كائنات Kotlin.

ما هي أفضل الممارسات لاستخدام Coroutines؟

تتيح لنا Coroutines التعامل مع العمليات غير المتزامنة بشكل أكثر كفاءة من الخيوط التقليدية، مما يجعل التطبيقات أكثر استجابة وأقل استهلاكًا للموارد.
تعليقات