NewsAPI Android
Aplicación de noticias con arquitectura MVVM, Retrofit para APIs, coroutines y diseño Material Design
Descripción del Proyecto
NewsAPI Android es una aplicación moderna de noticias que demuestra las mejores prácticas de desarrollo Android. Utiliza la arquitectura MVVM, Kotlin Coroutines para operaciones asíncronas, Retrofit para consumir APIs REST, y ViewBinding para una interfaz de usuario segura y eficiente.
La aplicación consume la API pública de NewsAPI.org para mostrar noticias actualizadas de diferentes fuentes y categorías, implementando funcionalidades como búsqueda, filtros y gestión de favoritos.
Capturas de Pantalla
Lista principal de noticias
Detalle de artículo completo
Navegación por categorías
Tecnologías Utilizadas
MVVM Architecture
Patrón arquitectónico limpio
Retrofit
Cliente HTTP para APIs
Kotlin Coroutines
Programación asíncrona
ViewBinding
Binding seguro de vistas
Fragment Lifecycle
Gestión de ciclo de vida
ViewModel
Gestión de datos UI
Características Principales
Noticias en Tiempo Real
Acceso a las últimas noticias de múltiples fuentes internacionales y locales.
Búsqueda Avanzada
Búsqueda de artículos por palabras clave con filtros de fecha y fuente.
Categorías
Organización por categorías: tecnología, deportes, entretenimiento, salud, etc.
Favoritos
Guarda artículos favoritos para lectura offline con persistencia local.
Actualización Automática
Refresh automático y manual de contenido con indicadores visuales.
Modo Oscuro
Soporte completo para tema claro y oscuro con transiciones suaves.
Arquitectura MVVM
Repository Pattern
class NewsRepository(
private val newsApiService: NewsApiService,
private val localDatabase: NewsDatabase
) {
suspend fun getTopHeadlines(
country: String = "us",
category: String? = null
): Result<List<Article>> {
return try {
val response = newsApiService.getTopHeadlines(
country = country,
category = category,
apiKey = BuildConfig.NEWS_API_KEY
)
if (response.isSuccessful) {
response.body()?.articles?.let { articles ->
// Cache en base de datos local
localDatabase.articleDao().insertAll(articles)
Result.success(articles)
} ?: Result.failure(Exception("Empty response"))
} else {
Result.failure(Exception("API Error: ${response.code()}"))
}
} catch (e: Exception) {
// Fallback a datos locales
val cachedArticles = localDatabase.articleDao().getAllArticles()
if (cachedArticles.isNotEmpty()) {
Result.success(cachedArticles)
} else {
Result.failure(e)
}
}
}
}
ViewModel con Coroutines
class NewsViewModel(
private val repository: NewsRepository
) : ViewModel() {
private val _newsState = MutableLiveData<UiState<List<Article>>>()
val newsState: LiveData<UiState<List<Article>>> = _newsState
private val _isRefreshing = MutableLiveData<Boolean>()
val isRefreshing: LiveData<Boolean> = _isRefreshing
fun loadNews(category: String? = null, forceRefresh: Boolean = false) {
viewModelScope.launch {
_isRefreshing.value = true
_newsState.value = UiState.Loading
repository.getTopHeadlines(category = category)
.onSuccess { articles ->
_newsState.value = UiState.Success(articles)
}
.onFailure { error ->
_newsState.value = UiState.Error(error.message ?: "Unknown error")
}
_isRefreshing.value = false
}
}
fun searchNews(query: String) {
viewModelScope.launch {
_newsState.value = UiState.Loading
repository.searchNews(query)
.onSuccess { articles ->
_newsState.value = UiState.Success(articles)
}
.onFailure { error ->
_newsState.value = UiState.Error(error.message ?: "Search failed")
}
}
}
}
Fragment con ViewBinding
class NewsFragment : Fragment() {
private var _binding: FragmentNewsBinding? = null
private val binding get() = _binding!!
private val viewModel: NewsViewModel by viewModels()
private lateinit var newsAdapter: NewsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentNewsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupSwipeRefresh()
observeViewModel()
viewModel.loadNews()
}
private fun setupRecyclerView() {
newsAdapter = NewsAdapter { article ->
findNavController().navigate(
NewsFragmentDirections.actionNewsToDetail(article)
)
}
binding.recyclerViewNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(context)
addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
)
}
}
private fun observeViewModel() {
viewModel.newsState.observe(viewLifecycleOwner) { state ->
when (state) {
is UiState.Loading -> {
binding.progressBar.isVisible = true
binding.recyclerViewNews.isVisible = false
}
is UiState.Success -> {
binding.progressBar.isVisible = false
binding.recyclerViewNews.isVisible = true
newsAdapter.submitList(state.data)
}
is UiState.Error -> {
binding.progressBar.isVisible = false
showError(state.message)
}
}
}
}
}
Integración con NewsAPI
Retrofit Service
interface NewsApiService {
@GET("top-headlines")
suspend fun getTopHeadlines(
@Query("country") country: String = "us",
@Query("category") category: String? = null,
@Query("apiKey") apiKey: String
): Response<NewsResponse>
@GET("everything")
suspend fun searchNews(
@Query("q") query: String,
@Query("sortBy") sortBy: String = "publishedAt",
@Query("apiKey") apiKey: String
): Response<NewsResponse>
@GET("sources")
suspend fun getSources(
@Query("apiKey") apiKey: String
): Response<SourcesResponse>
}
Data Models
@Parcelize
data class Article(
val source: Source,
val author: String?,
val title: String,
val description: String?,
val url: String,
val urlToImage: String?,
val publishedAt: String,
val content: String?
) : Parcelable
@Parcelize
data class Source(
val id: String?,
val name: String
) : Parcelable
data class NewsResponse(
val status: String,
val totalResults: Int,
val articles: List<Article>
)
Optimizaciones de Rendimiento
- Caché Local: Almacenamiento local con Room para acceso offline
- Image Loading: Carga eficiente de imágenes con Glide y caché
- Pagination: Carga incremental de artículos
- Coroutines: Operaciones no bloqueantes en hilo principal
- ViewBinding: Eliminación de findViewById costosos
- LiveData: Actualizaciones automáticas solo cuando el Fragment está activo
Desafíos y Soluciones
1. Gestión de Estados de Red
Desafío: Manejar diferentes estados (carga, éxito, error, sin conexión).
Solución: Implementación de sealed class UiState para estados claros y tipo-seguros.
2. Caché y Sincronización
Desafío: Mantener datos actualizados mientras se proporciona acceso offline.
Solución: Strategy pattern con fallback automático a datos locales cuando falla la red.
3. Performance en Listas Largas
Desafío: Mantener fluidez con cientos de artículos de noticias.
Solución: RecyclerView con ViewHolder optimizado y carga lazy de imágenes.