Aplicación Android Completado

NewsAPI Android

Aplicación de noticias con arquitectura MVVM, Retrofit para APIs, coroutines y diseño Material Design

NewsAPI Android App

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

Pantalla principal

Lista principal de noticias

Vista de detalle

Detalle de artículo completo

Categorías

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.