إنشاء مشروع بإستخدام Retrofit و MvvM و Hilt و coroutines في Kotlin Compose
في هذه المقالة، سنتعلم كيفية إنشاء مشروع كامل باستخدام عدة مكتبات مثل Retrofit لإدارة واجهة برمجة التطبيقات (API)، وMVVM كهندسة لتنظيم التعليمات البرمجية، وHilt لإدارة التبعيات، وCoroutines للتعامل مع العمليات غير المتزامنة. سنقوم ببناء المشروع خطوة بخطو. ستساعدك هذه المقالة على فهم كيفية استخدام هذه المكتبات معًا لإنشاء تطبيق فعال وقابل للتطوير.
خطوات إنشاء المشروع:
1. إنشاء وحدات
الخطوة الأولى هي إنشاء وحدات في المشروع باستخدام واجهة Android Studio. لنبدأ بالنقر بزر الماوس الأيمن على المشروع واختيار "وحدة جديدة"، ثم اختيار مكتبة Android وتسميتها "utils".
2. إضافة الخدمات
داخل وحدة utils، نضع خدمات مثل التحكم في اتصال الإنترنت أو حالات استجابة واجهة برمجة التطبيقات (API). على سبيل المثال:
اولا سوف نحتاج لعمل modules عن طريق Right Click ومن ثم اختيار Moduls
بعدها اختر من القائمه Android Library وضغ اسم لها وهنا قمت بعمل utils
سوف نضع فيها 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>()
}
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)
[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
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
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) }
}
}
@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 لمشروعك
محتوى Domain
interface NewsRepo {
suspend fun getNews(country:String) : Response<NewsResponse>
}
محتويات Data
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)
}
}
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
@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
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(),
)
}
}
}