From efb8d3918edf06d5cf445a7d560cf05d65920d5c Mon Sep 17 00:00:00 2001 From: Krunal Patel Date: Thu, 8 Jun 2023 18:46:54 +0530 Subject: [PATCH] TE7-T893: Add Web service without library - OkHttpClient - Json Parsing --- .../demo/appcomponents/utils/IntentData.kt | 1 + .../repositories/ComponentDetailRepository.kt | 3 + .../data/repositories/WebViewRepository.kt | 2 +- .../ui/fragments/WebViewFragment.kt | 3 + .../adapters/ViewBindingAdapter.kt | 21 +++++++ .../com/krunal/demo/utils/AppConstants.kt | 7 +++ .../withoutlibrary/data/apis/NewsApi.kt | 63 +++++++++++++++++++ .../data/models/api/ArticleResponse.kt | 12 ++++ .../data/models/api/NewsResponse.kt | 7 +++ .../data/models/api/SourceResponse.kt | 6 ++ .../data/models/local/NewsItem.kt | 8 +++ .../repositories/NewsRepository.kt | 10 +++ .../withoutlibrary/ui/adapters/NewsAdapter.kt | 45 +++++++++++++ .../ui/fragments/NewsListFragment.kt | 63 +++++++++++++++++++ .../ui/viewmodels/NewsViewModel.kt | 29 +++++++++ .../main/res/layout/fragment_news_list.xml | 31 +++++++++ Demo/app/src/main/res/layout/item_news.xml | 56 +++++++++++++++++ Demo/app/src/main/res/values/dimens.xml | 1 + Demo/app/src/main/res/values/strings.xml | 1 + 19 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/ArticleResponse.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/NewsResponse.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/SourceResponse.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/local/NewsItem.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/repositories/NewsRepository.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/adapters/NewsAdapter.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/fragments/NewsListFragment.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/viewmodels/NewsViewModel.kt create mode 100644 Demo/app/src/main/res/layout/fragment_news_list.xml create mode 100644 Demo/app/src/main/res/layout/item_news.xml diff --git a/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt b/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt index faaa8df..f62f43a 100644 --- a/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt +++ b/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt @@ -7,6 +7,7 @@ object IntentData { */ const val MESSAGE = "message" const val AUTH_KEY = "auth_key" + const val WEB_URL = "web_url" /** * Actions diff --git a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt index 7b8151a..5659dc3 100644 --- a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt +++ b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt @@ -34,6 +34,7 @@ import com.krunal.demo.uicomponents.constraintLayouts.GuidelineBarrierFragment import com.krunal.demo.uicomponents.constraintLayouts.RelativeFragment import com.krunal.demo.uicomponents.picker.DatePickerFragment import com.krunal.demo.uicomponents.picker.TimePickerFragment +import com.krunal.demo.webservices.withoutlibrary.ui.fragments.NewsListFragment object ComponentDetailRepository { @@ -55,6 +56,8 @@ object ComponentDetailRepository { "App Component", appComponents ), ComponentDetail.ActivityComponent( "Search and Web view", SearchWebActivity::class.java + ), ComponentDetail.FragmentComponent( + "News List", NewsListFragment::class.java ) ) } diff --git a/Demo/app/src/main/java/com/krunal/demo/searchwebview/data/repositories/WebViewRepository.kt b/Demo/app/src/main/java/com/krunal/demo/searchwebview/data/repositories/WebViewRepository.kt index 01ade6b..f7f32f1 100644 --- a/Demo/app/src/main/java/com/krunal/demo/searchwebview/data/repositories/WebViewRepository.kt +++ b/Demo/app/src/main/java/com/krunal/demo/searchwebview/data/repositories/WebViewRepository.kt @@ -19,5 +19,5 @@ object WebViewRepository { "m.youtube.com" ) - fun getRandomUrl(): String = urls.random() + fun getRandomUrl(): String = "https://google.com"//urls.random() } \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/searchwebview/ui/fragments/WebViewFragment.kt b/Demo/app/src/main/java/com/krunal/demo/searchwebview/ui/fragments/WebViewFragment.kt index 3f01b84..e732285 100644 --- a/Demo/app/src/main/java/com/krunal/demo/searchwebview/ui/fragments/WebViewFragment.kt +++ b/Demo/app/src/main/java/com/krunal/demo/searchwebview/ui/fragments/WebViewFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.krunal.demo.appcomponents.utils.IntentData import com.krunal.demo.databinding.FragmentWebViewBinding import com.krunal.demo.searchwebview.helpers.WebClient import com.krunal.demo.searchwebview.ui.viewmodels.WebViewViewModel @@ -41,6 +42,8 @@ class WebViewFragment : Fragment() { @SuppressLint("SetJavaScriptEnabled") private fun setupWebView() { + arguments?.getString(IntentData.WEB_URL)?.let(viewModel::loadUrl) + binding.webView.apply { webViewClient = WebClient() settings.javaScriptEnabled = true diff --git a/Demo/app/src/main/java/com/krunal/demo/uicomponents/adapters/ViewBindingAdapter.kt b/Demo/app/src/main/java/com/krunal/demo/uicomponents/adapters/ViewBindingAdapter.kt index 548048c..aabe481 100644 --- a/Demo/app/src/main/java/com/krunal/demo/uicomponents/adapters/ViewBindingAdapter.kt +++ b/Demo/app/src/main/java/com/krunal/demo/uicomponents/adapters/ViewBindingAdapter.kt @@ -1,7 +1,9 @@ package com.krunal.demo.uicomponents.adapters +import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.drawable.GradientDrawable +import android.media.ThumbnailUtils import android.view.View import android.widget.ImageView import android.widget.TextView @@ -10,6 +12,12 @@ import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.ColorUtils import androidx.databinding.BindingAdapter +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.net.HttpURLConnection +import java.net.URL @BindingAdapter("drawableResource") fun ImageView.setDrawableResource(@DrawableRes drawableResource: Int) { @@ -58,4 +66,17 @@ fun View.setGradientBackground(gradientStartColor: Int?, gradientEndColor: Int?, } } } +} + +@BindingAdapter("imageUrl") +fun ImageView.bindImage(url: String?) { + if (url == null) return + + findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.IO) { + runCatching { + val urlConnection = URL(url).openConnection() as HttpURLConnection + val bitmap = BitmapFactory.decodeStream(urlConnection.inputStream) + setImageBitmap(ThumbnailUtils.extractThumbnail(bitmap, 200, 200)) + } + } } \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt b/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt new file mode 100644 index 0000000..3b37918 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt @@ -0,0 +1,7 @@ +package com.krunal.demo.utils + +object AppConstants { + + const val NEWS_API_KEY = "fa3d5d7db3f8448dac9f98e82f1108ce" + const val NEST_BASE_URL = "https://newsapi.org/v2" +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt new file mode 100644 index 0000000..ec34063 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt @@ -0,0 +1,63 @@ +package com.krunal.demo.webservices.withoutlibrary.data.apis + +import com.krunal.demo.utils.AppConstants +import com.krunal.demo.webservices.withoutlibrary.data.models.api.ArticleResponse +import com.krunal.demo.webservices.withoutlibrary.data.models.api.NewsResponse +import com.krunal.demo.webservices.withoutlibrary.data.models.api.SourceResponse +import org.json.JSONObject +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL + +object NewsApi { + + private const val HEADLINE_URL = + "${AppConstants.NEST_BASE_URL}/top-headlines?country=in&apiKey=${AppConstants.NEWS_API_KEY}" + + suspend fun getNews(): NewsResponse? { + var newsResponse: NewsResponse? = null + + runCatching { + val url = URL(HEADLINE_URL) + val urlConnection = url.openConnection() as HttpURLConnection + urlConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:221.0) Gecko/20100101 Firefox/31.0") + val br = BufferedReader(InputStreamReader(urlConnection.inputStream)) + val response = br.readText() + + val newsJson = JSONObject(response) + val status = newsJson.getString("status") + val totalResults = newsJson.getInt("totalResults") + + val articles = mutableListOf() + + val articlesJson = newsJson.getJSONArray("articles") + + for (i in 0 until articlesJson.length()) { + if (i == 19) { + print("") + } + val articleJson = articlesJson.getJSONObject(i) + val sourceJson = articleJson.getJSONObject("source") + val source = + SourceResponse(sourceJson.optString("id"), sourceJson.optString("name")) + val article = ArticleResponse( + source = source, + author = articleJson.optString("author"), + title = articleJson.getString("title"), + description = articleJson.getString("description"), + url = articleJson.getString("url"), + urlToImage = articleJson.getString("urlToImage"), + publishedAt = articleJson.getString("publishedAt"), + content = articleJson.getString("content"), + ) + articles.add(article) + } + + newsResponse = NewsResponse( + status, totalResults, articles + ) + } + return newsResponse + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/ArticleResponse.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/ArticleResponse.kt new file mode 100644 index 0000000..660bb3f --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/ArticleResponse.kt @@ -0,0 +1,12 @@ +package com.krunal.demo.webservices.withoutlibrary.data.models.api + +data class ArticleResponse( + val source: SourceResponse, + val author: String? = null, + val title: String, + val description: String, + val url: String, + val urlToImage: String, + val publishedAt: String, + val content: String +) diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/NewsResponse.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/NewsResponse.kt new file mode 100644 index 0000000..e343e91 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/NewsResponse.kt @@ -0,0 +1,7 @@ +package com.krunal.demo.webservices.withoutlibrary.data.models.api + +data class NewsResponse( + val status: String, + val totalResult: Int, + val articles: List +) diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/SourceResponse.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/SourceResponse.kt new file mode 100644 index 0000000..0d63744 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/api/SourceResponse.kt @@ -0,0 +1,6 @@ +package com.krunal.demo.webservices.withoutlibrary.data.models.api + +data class SourceResponse( + val id: String? = null, + val name: String? = null +) diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/local/NewsItem.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/local/NewsItem.kt new file mode 100644 index 0000000..34c1c32 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/models/local/NewsItem.kt @@ -0,0 +1,8 @@ +package com.krunal.demo.webservices.withoutlibrary.data.models.local + +data class NewsItem( + val title: String, + val description: String, + val url: String, + val imageUrl: String +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/repositories/NewsRepository.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/repositories/NewsRepository.kt new file mode 100644 index 0000000..33939d4 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/repositories/NewsRepository.kt @@ -0,0 +1,10 @@ +package com.krunal.demo.webservices.withoutlibrary.repositories + +import com.krunal.demo.webservices.withoutlibrary.data.apis.NewsApi +import com.krunal.demo.webservices.withoutlibrary.data.models.api.ArticleResponse + +object NewsRepository { + + suspend fun getNewsArticles(): List = + NewsApi.getNews()?.articles ?: emptyList() +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/adapters/NewsAdapter.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/adapters/NewsAdapter.kt new file mode 100644 index 0000000..78ea59d --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/adapters/NewsAdapter.kt @@ -0,0 +1,45 @@ +package com.krunal.demo.webservices.withoutlibrary.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.krunal.demo.databinding.ItemNewsBinding +import com.krunal.demo.webservices.withoutlibrary.data.models.local.NewsItem + +class NewsAdapter(val onClick: (newsItem: NewsItem) -> Unit) : RecyclerView.Adapter() { + + private val newsList: MutableList = mutableListOf() + + class NewsViewHolder( + private val binding: ItemNewsBinding, + val onClick: (NewsItem) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(newsItem: NewsItem) { + binding.newsItem = newsItem + binding.root.setOnClickListener { + onClick(newsItem) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ItemNewsBinding.inflate(layoutInflater, parent, false) + return NewsViewHolder(binding, onClick) + } + + override fun getItemCount(): Int = newsList.count() + + override fun onBindViewHolder(holder: NewsViewHolder, position: Int) { + holder.bind(newsList[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun submitList(list: List) { + newsList.clear() + newsList.addAll(list) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/fragments/NewsListFragment.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/fragments/NewsListFragment.kt new file mode 100644 index 0000000..3be5263 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/fragments/NewsListFragment.kt @@ -0,0 +1,63 @@ +package com.krunal.demo.webservices.withoutlibrary.ui.fragments + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.net.toUri +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import com.krunal.demo.databinding.FragmentNewsListBinding +import com.krunal.demo.webservices.withoutlibrary.ui.adapters.NewsAdapter +import com.krunal.demo.webservices.withoutlibrary.ui.viewmodels.NewsViewModel +import kotlinx.coroutines.launch + +class NewsListFragment : Fragment() { + + private lateinit var binding: FragmentNewsListBinding + private val viewModel: NewsViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + binding = FragmentNewsListBinding.inflate(layoutInflater, container, false) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUI() + } + + private fun setupUI() { + val adapter = NewsAdapter { newsItem -> + Intent(Intent.ACTION_VIEW, newsItem.url.toUri()).also { intent -> + activity?.startActivity(intent) + } + } + + binding.rvNewsList.apply { + this.adapter = adapter + addItemDecoration( + DividerItemDecoration( + requireContext(), DividerItemDecoration.VERTICAL + ) + ) + } + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.newsList.collect { list -> + adapter.submitList(list) + } + } + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/viewmodels/NewsViewModel.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/viewmodels/NewsViewModel.kt new file mode 100644 index 0000000..6c44f23 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/ui/viewmodels/NewsViewModel.kt @@ -0,0 +1,29 @@ +package com.krunal.demo.webservices.withoutlibrary.ui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.krunal.demo.webservices.withoutlibrary.data.models.local.NewsItem +import com.krunal.demo.webservices.withoutlibrary.repositories.NewsRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class NewsViewModel : ViewModel() { + + private val _newsList: MutableStateFlow> = MutableStateFlow(emptyList()) + val newsList: StateFlow> = _newsList + + init { + loadData() + } + + private fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + _newsList.emit( + NewsRepository.getNewsArticles() + .map { NewsItem(it.title, it.description, it.url, it.urlToImage) } + ) + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/res/layout/fragment_news_list.xml b/Demo/app/src/main/res/layout/fragment_news_list.xml new file mode 100644 index 0000000..6c7b169 --- /dev/null +++ b/Demo/app/src/main/res/layout/fragment_news_list.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/layout/item_news.xml b/Demo/app/src/main/res/layout/item_news.xml new file mode 100644 index 0000000..1e2103e --- /dev/null +++ b/Demo/app/src/main/res/layout/item_news.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/values/dimens.xml b/Demo/app/src/main/res/values/dimens.xml index aae23bd..f5336f7 100644 --- a/Demo/app/src/main/res/values/dimens.xml +++ b/Demo/app/src/main/res/values/dimens.xml @@ -42,4 +42,5 @@ 52dp 15sp 4dp + 130dp \ No newline at end of file diff --git a/Demo/app/src/main/res/values/strings.xml b/Demo/app/src/main/res/values/strings.xml index a0e8dfe..2e7d03a 100644 --- a/Demo/app/src/main/res/values/strings.xml +++ b/Demo/app/src/main/res/values/strings.xml @@ -224,4 +224,5 @@ Application icon Search app or package name Toggle Dark Mode + Validate \ No newline at end of file