diff --git a/app/build.gradle b/app/build.gradle
index 0d0fa1b..2643cc6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,6 +31,10 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+
+ viewBinding {
+ enabled = true
+ }
}
dependencies {
@@ -40,6 +44,8 @@ dependencies {
implementation libs.material
implementation libs.androidx.activity
implementation libs.androidx.constraintlayout
+ implementation libs.androidx.recyclerview
+ implementation libs.swipe.refresh.layout
// dagger
implementation libs.dagger
kapt libs.dagger.compiler
@@ -47,6 +53,7 @@ dependencies {
//retrofit
implementation libs.retrofit
implementation libs.retrofit.rx.adapter
+ implementation libs.converter.gson
//glide
implementation libs.glide
@@ -57,6 +64,8 @@ dependencies {
implementation libs.rx.kotlin
testImplementation libs.junit
+ testImplementation libs.mockito.core
+ testImplementation libs.mockito.kotlin
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8a18c7a..2872bf6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,26 +1,29 @@
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
+
-
-
-
-
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/otchallenge/Constants.kt b/app/src/main/java/com/example/otchallenge/Constants.kt
new file mode 100644
index 0000000..cb4831d
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/Constants.kt
@@ -0,0 +1,3 @@
+package com.example.otchallenge
+
+const val BOOKS_API_KEY="KoRB4K5LRHygfjCL2AH6iQ7NeUqDAGAB"
diff --git a/app/src/main/java/com/example/otchallenge/MainActivity.kt b/app/src/main/java/com/example/otchallenge/MainActivity.kt
index d35da32..3b4dcb1 100644
--- a/app/src/main/java/com/example/otchallenge/MainActivity.kt
+++ b/app/src/main/java/com/example/otchallenge/MainActivity.kt
@@ -1,22 +1,79 @@
package com.example.otchallenge
import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+import android.view.LayoutInflater
+import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-
-class MainActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- (application as MyApplication).appComponent.inject(this)
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContentView(R.layout.activity_main)
- ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
- val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
- v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
- insets
- }
- }
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.example.otchallenge.databinding.ActivityMainBinding
+import com.example.otchallenge.modules.DataProvider
+import com.example.otchallenge.model.BookModel
+import com.example.otchallenge.modules.ImageProvider
+import com.example.otchallenge.presenter.BooksPresenter
+import com.example.otchallenge.presenter.BooksPresenterInterface
+import com.example.otchallenge.ui.BooksAdapter
+import javax.inject.Inject
+
+class MainActivity : AppCompatActivity(), BooksPresenterInterface {
+
+ @Inject
+ lateinit var imageProvider: ImageProvider
+
+ @Inject
+ lateinit var dataProvider: DataProvider
+
+ private lateinit var binding: ActivityMainBinding
+
+ private lateinit var presenter: BooksPresenter
+
+ private lateinit var adapter: BooksAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ (application as MyApplication).appComponent.inject(this)
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
+ setContentView(binding.root)
+ initUi()
+ }
+
+ private fun initUi() {
+ binding.recyclerView.layoutManager = LinearLayoutManager(binding.root.context)
+ adapter = BooksAdapter(imageProvider)
+ binding.recyclerView.adapter = adapter
+ binding.retryButton.setOnClickListener {
+ // retry to get the information
+ dataProvider.getBooks()
+ }
+ binding.swipeRefreshLayout.setOnRefreshListener {
+ // refresh the information
+ dataProvider.getBooks()
+ }
+ // due the strong reference between ui and presenter, presenter should live
+ // only in the lifecycle scope with the view
+ presenter = BooksPresenter(this@MainActivity)
+ dataProvider.booksData.observe(this@MainActivity) {
+ // display the information with the presenter
+ presenter.presentUi(it)
+ }
+ // as view has been created we can request the initial data
+ dataProvider.getBooks()
+ }
+
+ override fun displayBooks(books: List) {
+ adapter.updateData(books)
+ binding.retryButton.isVisible = false
+ }
+
+ override fun displayLoader(showLoader: Boolean) {
+ binding.swipeRefreshLayout.isRefreshing = showLoader
+ }
+
+ override fun displayError(message: String?) {
+ binding.swipeRefreshLayout.isRefreshing = false
+ binding.retryButton.isVisible = true
+ message?.let {
+ Toast.makeText(this@MainActivity, it, Toast.LENGTH_LONG).show()
+ }
+ }
}
diff --git a/app/src/main/java/com/example/otchallenge/MyApplication.kt b/app/src/main/java/com/example/otchallenge/MyApplication.kt
index ba66e70..e144604 100644
--- a/app/src/main/java/com/example/otchallenge/MyApplication.kt
+++ b/app/src/main/java/com/example/otchallenge/MyApplication.kt
@@ -3,13 +3,20 @@ package com.example.otchallenge
import android.app.Application
import com.example.otchallenge.di.AppComponent
import com.example.otchallenge.di.DaggerAppComponent
+import com.example.otchallenge.di.DataProviderModule
+import com.example.otchallenge.di.ImageProviderModule
+import com.example.otchallenge.di.NetworkProviderModule
class MyApplication : Application() {
- lateinit var appComponent: AppComponent
+ lateinit var appComponent: AppComponent
- override fun onCreate() {
- super.onCreate()
- appComponent = DaggerAppComponent.builder().build()
- }
+ override fun onCreate() {
+ super.onCreate()
+ appComponent = DaggerAppComponent.builder()
+ .networkProviderModule(NetworkProviderModule())
+ .imageProviderModule(ImageProviderModule(this))
+ .dataProviderModule(DataProviderModule())
+ .build()
+ }
}
diff --git a/app/src/main/java/com/example/otchallenge/api/BooksApi.kt b/app/src/main/java/com/example/otchallenge/api/BooksApi.kt
new file mode 100644
index 0000000..13a290f
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/api/BooksApi.kt
@@ -0,0 +1,12 @@
+package com.example.otchallenge.api
+
+import com.example.otchallenge.BOOKS_API_KEY
+import com.example.otchallenge.model.BooksResponseModel
+import io.reactivex.Single
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface BooksApi {
+ @GET("svc/books/v3/lists/current/hardcover-fiction.json?&api-key=${BOOKS_API_KEY}")
+ fun getBooks(@Query("offset") offset: Int = 0): Single
+}
diff --git a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt
index d3c83c6..2a3acf9 100644
--- a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt
+++ b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt
@@ -5,7 +5,13 @@ import dagger.Component
import javax.inject.Singleton
@Singleton
-@Component
+@Component(
+ modules = [
+ NetworkProviderModule::class,
+ DataProviderModule::class,
+ ImageProviderModule::class
+ ]
+)
interface AppComponent {
- fun inject(activity: MainActivity)
+ fun inject(activity: MainActivity)
}
diff --git a/app/src/main/java/com/example/otchallenge/di/DataProviderModule.kt b/app/src/main/java/com/example/otchallenge/di/DataProviderModule.kt
new file mode 100644
index 0000000..b5fa52e
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/di/DataProviderModule.kt
@@ -0,0 +1,17 @@
+package com.example.otchallenge.di
+
+import com.example.otchallenge.api.BooksApi
+import com.example.otchallenge.modules.DataProvider
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class DataProviderModule {
+
+ @Provides
+ @Singleton
+ fun provideDataProvider(api: BooksApi): DataProvider {
+ return DataProvider(api)
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/di/ImageProviderModule.kt b/app/src/main/java/com/example/otchallenge/di/ImageProviderModule.kt
new file mode 100644
index 0000000..485419f
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/di/ImageProviderModule.kt
@@ -0,0 +1,15 @@
+package com.example.otchallenge.di
+
+import android.content.Context
+import com.example.otchallenge.modules.ImageProvider
+import dagger.Module
+import dagger.Provides
+
+@Module
+class ImageProviderModule(private val context: Context) {
+
+ @Provides
+ fun provideImageProvider(): ImageProvider {
+ return ImageProvider(context)
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/di/NetworkProviderModule.kt b/app/src/main/java/com/example/otchallenge/di/NetworkProviderModule.kt
new file mode 100644
index 0000000..054f7c7
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/di/NetworkProviderModule.kt
@@ -0,0 +1,29 @@
+package com.example.otchallenge.di
+
+import com.example.otchallenge.api.BooksApi
+import dagger.Module
+import dagger.Provides
+import retrofit2.Retrofit
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+@Module
+class NetworkProviderModule {
+
+ @Provides
+ @Singleton
+ fun provideRetrofit(): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl("https://api.nytimes.com/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideApi(retrofit: Retrofit): BooksApi {
+ return retrofit.create(BooksApi::class.java)
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/model/BookModel.kt b/app/src/main/java/com/example/otchallenge/model/BookModel.kt
new file mode 100644
index 0000000..a282625
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/model/BookModel.kt
@@ -0,0 +1,10 @@
+package com.example.otchallenge.model
+
+import com.google.gson.annotations.SerializedName
+
+data class BookModel(
+ val title: String? = null,
+ @SerializedName("book_image")
+ val image: String? = null,
+ val description: String? = null
+)
diff --git a/app/src/main/java/com/example/otchallenge/model/BooksData.kt b/app/src/main/java/com/example/otchallenge/model/BooksData.kt
new file mode 100644
index 0000000..0898d82
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/model/BooksData.kt
@@ -0,0 +1,9 @@
+package com.example.otchallenge.model
+
+data class BooksData(
+ val isLoading: Boolean = false,
+ val shouldLoadMore: Boolean = false,
+ val books: List = emptyList(),
+ val showError: Boolean = false,
+ val errorMessage: String? = null,
+)
diff --git a/app/src/main/java/com/example/otchallenge/model/BooksResponseModel.kt b/app/src/main/java/com/example/otchallenge/model/BooksResponseModel.kt
new file mode 100644
index 0000000..4350a9f
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/model/BooksResponseModel.kt
@@ -0,0 +1,11 @@
+package com.example.otchallenge.model
+
+import com.google.gson.annotations.SerializedName
+
+data class BooksResponseModel(
+ @SerializedName("num_results")
+ val numResults: Int? = 0,
+ val results: BooksResultModel? = null,
+)
+
+data class BooksResultModel(val books: List)
diff --git a/app/src/main/java/com/example/otchallenge/modules/DataProvider.kt b/app/src/main/java/com/example/otchallenge/modules/DataProvider.kt
new file mode 100644
index 0000000..58ee105
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/modules/DataProvider.kt
@@ -0,0 +1,56 @@
+package com.example.otchallenge.modules
+
+import androidx.lifecycle.MutableLiveData
+import com.example.otchallenge.api.BooksApi
+import com.example.otchallenge.model.BooksData
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+
+/**
+ * Data Provider class
+ *
+ * the data provider of the application, his duty is to retrieve and provide
+ * the data to be displayed.
+ *
+ * Note: open class for testing purposes
+ */
+open class DataProvider(private val api: BooksApi) {
+
+ open val booksData: MutableLiveData by lazy {
+ MutableLiveData(BooksData())
+ }
+
+ open fun getBooks() {
+ // disposable should be disposed when view is destroyed
+ val disposable = api.getBooks()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnSubscribe {
+ booksData.postValue(
+ booksData.value?.copy(
+ isLoading = true,
+ showError = false,
+ errorMessage = null,
+ )
+ )
+ }
+ .subscribe({
+ booksData.postValue(
+ booksData.value?.copy(
+ isLoading = false,
+ books = it.results?.books ?: emptyList(),
+ showError = false,
+ errorMessage = null,
+ )
+ )
+ }, {
+ booksData.postValue(
+ booksData.value?.copy(
+ isLoading = false,
+ showError = true,
+ errorMessage = it.message,
+ )
+ )
+ })
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/modules/ImageProvider.kt b/app/src/main/java/com/example/otchallenge/modules/ImageProvider.kt
new file mode 100644
index 0000000..5c52a0b
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/modules/ImageProvider.kt
@@ -0,0 +1,27 @@
+package com.example.otchallenge.modules
+
+import android.content.Context
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.example.otchallenge.R
+
+/**
+ * this is the image provider implementation created to decouple the image
+ * library from the logical code, receives teh context to share the cache and speed up load
+ *
+ * during load will display launcher as place holder, this is useful in low speed connections
+ *
+ * Note: open class for mock testing purposes
+ */
+open class ImageProvider(private val context: Context) {
+
+ open fun displayImage(view: ImageView?, url: String? = null) {
+ if (url.isNullOrEmpty() || view == null) {
+ return
+ }
+ Glide.with(context)
+ .load(url)
+ .placeholder(R.drawable.ic_launcher_foreground)
+ .into(view)
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/presenter/BooksPresenter.kt b/app/src/main/java/com/example/otchallenge/presenter/BooksPresenter.kt
new file mode 100644
index 0000000..84982dc
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/presenter/BooksPresenter.kt
@@ -0,0 +1,20 @@
+package com.example.otchallenge.presenter
+
+import com.example.otchallenge.model.BooksData
+
+class BooksPresenter(private val presenterInterface: BooksPresenterInterface) {
+
+ fun presentUi(data: BooksData) {
+ if (data.showError) {
+ presenterInterface.displayError(data.errorMessage)
+ } else {
+ presenterInterface.displayLoader(data.isLoading)
+ if (data.books.isEmpty()) {
+ // ideally we should display an empty data message
+ presenterInterface.displayBooks(emptyList())
+ } else {
+ presenterInterface.displayBooks(data.books)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/otchallenge/presenter/BooksPresenterInterface.kt b/app/src/main/java/com/example/otchallenge/presenter/BooksPresenterInterface.kt
new file mode 100644
index 0000000..8f98c18
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/presenter/BooksPresenterInterface.kt
@@ -0,0 +1,17 @@
+package com.example.otchallenge.presenter
+
+import com.example.otchallenge.model.BookModel
+
+/**
+ * Books presenter interface
+ *
+ * interface that defines the contract between the view and the Presenter Class
+ */
+interface BooksPresenterInterface {
+
+ fun displayBooks(books: List)
+
+ fun displayLoader(showLoader: Boolean)
+
+ fun displayError(message: String?)
+}
diff --git a/app/src/main/java/com/example/otchallenge/ui/BooksAdapter.kt b/app/src/main/java/com/example/otchallenge/ui/BooksAdapter.kt
new file mode 100644
index 0000000..e4c61ce
--- /dev/null
+++ b/app/src/main/java/com/example/otchallenge/ui/BooksAdapter.kt
@@ -0,0 +1,44 @@
+package com.example.otchallenge.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.otchallenge.R
+import com.example.otchallenge.model.BookModel
+import com.example.otchallenge.modules.ImageProvider
+
+class BooksAdapter(
+ private val imageProvider: ImageProvider,
+) : RecyclerView.Adapter() {
+
+ private val books = mutableListOf()
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookVieHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ return BookVieHolder(layoutInflater.inflate(R.layout.book_item_view, parent, false))
+ }
+
+ override fun getItemCount() = books.count()
+
+ override fun onBindViewHolder(holder: BookVieHolder, position: Int) {
+ val bookToDisplay = books[position]
+ holder.title?.text = bookToDisplay.title
+ holder.description?.text = bookToDisplay.description
+ imageProvider.displayImage(holder.image, bookToDisplay.image)
+ }
+
+ fun updateData(newBooks: List) {
+ books.clear()
+ books.addAll(newBooks)
+ // we replaced all data so we have to call this even with warning
+ notifyDataSetChanged()
+ }
+}
+
+class BookVieHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val image: ImageView? = view.findViewById(R.id.imageView)
+ val title: TextView? = view.findViewById(R.id.title)
+ val description: TextView? = view.findViewById(R.id.description)
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index c7a2d54..1cd8bd1 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,29 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
-
+
-
\ No newline at end of file
+
+
+
+
+
diff --git a/app/src/main/res/layout/book_item_view.xml b/app/src/main/res/layout/book_item_view.xml
new file mode 100644
index 0000000..22f2d86
--- /dev/null
+++ b/app/src/main/res/layout/book_item_view.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/com/example/otchallenge/presenter/BooksPresenterTests.kt b/app/src/test/java/com/example/otchallenge/presenter/BooksPresenterTests.kt
new file mode 100644
index 0000000..b32d18e
--- /dev/null
+++ b/app/src/test/java/com/example/otchallenge/presenter/BooksPresenterTests.kt
@@ -0,0 +1,81 @@
+package com.example.otchallenge.presenter
+
+import com.example.otchallenge.model.BookModel
+import com.example.otchallenge.model.BooksData
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.*
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.*
+
+class BooksPresenterTests {
+
+ private lateinit var presenter: BooksPresenter
+ private lateinit var currentData: BooksData
+ @Mock
+ lateinit var interfaceMock : BooksPresenterInterface
+
+ @Before
+ fun setup() {
+ currentData = BooksData()
+ interfaceMock = mock()
+ presenter = BooksPresenter(interfaceMock)
+ }
+
+ @Test
+ fun testDefaultInfo() {
+ // given default data
+
+ whenPresenterPresentData()
+
+ // then
+ verify(interfaceMock, times(0)).displayError(anyString())
+ verify(interfaceMock).displayLoader(eq(false))
+ verify(interfaceMock).displayBooks(currentData.books)
+ }
+
+ @Test
+ fun testShouldDisplayLoaderWhenLoading() {
+ // given a data that is loading
+ currentData = currentData.copy(isLoading = true)
+
+ whenPresenterPresentData()
+
+ // then loader should be displayed
+ verify(interfaceMock).displayLoader(eq(true))
+ }
+
+ @Test
+ fun testShouldDisplayDataWhenBooksAvailable() {
+ // given data with books
+ val books = listOf(BookModel("Book", "Description"))
+ currentData = currentData.copy(books = books)
+
+ whenPresenterPresentData()
+
+ // then verify book is displayed
+ verify(interfaceMock).displayLoader(false)
+ verify(interfaceMock).displayBooks(books)
+ // no error is being invoked
+ verify(interfaceMock, times(0)).displayError(anyString())
+ }
+
+ @Test
+ fun testShouldDisplayError() {
+ // given a data that has error
+ currentData = currentData.copy(showError = true, errorMessage = "Error")
+
+ whenPresenterPresentData()
+
+ // then error should be displayed
+ verify(interfaceMock).displayError("Error")
+ // and no other methods should be called
+ verify(interfaceMock, times(0)).displayLoader(anyBoolean())
+ verify(interfaceMock, times(0)).displayBooks(anyList())
+ }
+
+ private fun whenPresenterPresentData() {
+ presenter.presentUi(currentData)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4664302..ef3c0b6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,6 +15,10 @@ glide = "4.16.0"
rx-android = "2.1.1"
rx-java = "2.2.21"
rx-kotlin = "2.4.0"
+recyclerview = "1.3.2"
+refreshLayout = "1.1.0"
+mockitoCore = "5.0.0"
+mockitoKotlin = "5.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -33,6 +37,11 @@ glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "gl
rx-java = {group = "io.reactivex.rxjava2", name="rxjava", version.ref = "rx-java"}
rx-android = {group = "io.reactivex.rxjava2", name="rxandroid", version.ref = "rx-android"}
rx-kotlin = {group = "io.reactivex.rxjava2", name="rxkotlin", version.ref = "rx-kotlin"}
+androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
+swipe-refresh-layout = { module = "androidx.swiperefreshlayout:swiperefreshlayout" , version.ref = "refreshLayout" }
+mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
+mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }