未验证 提交 4db2bd50 编写于 作者: D Dustin Lam 提交者: GitHub

Merge pull request #965 from dlam/dlam/fix-paging-test

Fix + add transformation example to PagingSample tests
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -50,6 +50,11 @@ android {
jvmTarget = "1.8"
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
}
packagingOptions {
pickFirst "META-INF/AL2.0"
pickFirst "META-INF/LGPL2.1"
}
}
dependencies {
......@@ -57,18 +62,18 @@ dependencies {
implementation deps.fragment.runtime_ktx
implementation deps.recyclerview
implementation deps.cardview
implementation deps.room.runtime
implementation deps.lifecycle.runtime
implementation deps.paging_runtime
implementation deps.kotlin.stdlib
kapt deps.room.compiler
implementation deps.room.runtime
// Android Testing Support Library's runner and rules
androidTestImplementation deps.atsl.core
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.atsl.runner
androidTestImplementation deps.atsl.rules
androidTestImplementation deps.arch_core.testing
androidTestImplementation deps.room.testing
kapt deps.room.compiler
androidTestImplementation deps.coroutines.test
androidTestImplementation deps.truth
androidTestImplementation deps.espresso.contrib
}
package paging.android.example.com.pagingsample
import androidx.paging.AsyncPagingDataDiffer
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.recyclerview.widget.ListUpdateCallback
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class CheeseViewModelTest {
private val testDispatcher = TestCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun separatorsTest() = runBlockingTest(testDispatcher) {
val cheeses = listOf(
Cheese(0, "Abbaye de Belloc"),
Cheese(1, "Brie"),
Cheese(2, "Cheddar"),
)
val differ = AsyncPagingDataDiffer(
diffCallback = CheeseAdapter.diffCallback,
updateCallback = noopListUpdateCallback,
mainDispatcher = testDispatcher,
workerDispatcher = testDispatcher,
)
val cheeseViewModel = CheeseViewModel(CheeseDaoFake(cheeses))
// submitData allows differ to receive data from PagingData, but suspends until
// invalidation, so we must launch this in a separate job.
val job = launch {
cheeseViewModel.allCheeses.collectLatest { pagingData ->
differ.submitData(pagingData)
}
}
// Wait for initial load to finish.
advanceUntilIdle()
assertThat(differ.snapshot()).containsExactly(
CheeseListItem.Separator('A'),
CheeseListItem.Item(cheeses[0]),
CheeseListItem.Separator('B'),
CheeseListItem.Item(cheeses[1]),
CheeseListItem.Separator('C'),
CheeseListItem.Item(cheeses[2]),
)
// runBlockingTest checks for leaking jobs, so we have to cancel the one we started.
job.cancel()
}
}
class CheeseDaoFake(val cheeses: List<Cheese>) : CheeseDao {
override fun allCheesesByName(): PagingSource<Int, Cheese> {
return object : PagingSource<Int, Cheese>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Cheese> {
return LoadResult.Page(
data = cheeses,
prevKey = null,
nextKey = null,
)
}
// Ignored in test.
override fun getRefreshKey(state: PagingState<Int, Cheese>): Int? = null
}
}
override fun insert(cheeses: List<Cheese>) {}
override fun insert(cheese: Cheese) {}
override fun delete(cheese: Cheese) {}
}
val noopListUpdateCallback = object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {}
override fun onRemoved(position: Int, count: Int) {}
override fun onMoved(fromPosition: Int, toPosition: Int) {}
override fun onChanged(position: Int, count: Int, payload: Any?) {}
}
......@@ -16,15 +16,16 @@
package paging.android.example.com.pagingsample
import android.content.Intent
import androidx.annotation.UiThread
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeoutException
/**
* Simply sanity test to ensure that activity launches without any issues and shows some data.
......@@ -33,16 +34,19 @@ import java.util.concurrent.TimeoutException
class MainActivityTest {
@Test
@Throws(InterruptedException::class, TimeoutException::class)
@UiThread
fun showSomeResults() {
val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val scenario = ActivityScenario.launch<MainActivity>(intent)
ActivityScenario.launch<MainActivity>(intent)
onView(withId(R.id.cheeseList)).check { view, noViewFoundException ->
if (noViewFoundException != null) {
throw noViewFoundException
}
scenario.onActivity { activity ->
val recyclerView: RecyclerView = activity.binding.cheeseList
MatcherAssert.assertThat(recyclerView.adapter, CoreMatchers.notNullValue())
MatcherAssert.assertThat(recyclerView.adapter!!.itemCount > 0, CoreMatchers.`is`(true))
val recyclerView = view as RecyclerView
assertThat(recyclerView.adapter).isNotNull()
assertThat(recyclerView.adapter!!.itemCount).isGreaterThan(0)
}
}
}
\ No newline at end of file
}
......@@ -16,53 +16,63 @@
package paging.android.example.com.pagingsample
import androidx.recyclerview.widget.DiffUtil
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
/**
* A simple PagedListAdapter that binds Cheese items into CardViews.
* <p>
*
* PagedListAdapter is a RecyclerView.Adapter base class which can present the content of PagedLists
* in a RecyclerView. It requests new pages as the user scrolls, and handles new PagedLists by
* computing list differences on a background thread, and dispatching minimal, efficient updates to
* the RecyclerView to ensure minimal UI thread work.
* <p>
*
* If you want to use your own Adapter base class, try using a PagedListAdapterHelper inside your
* adapter instead.
*
* @see androidx.paging.PagedListAdapter
* @see androidx.paging.AsyncPagedListDiffer
*/
class CheeseAdapter : PagingDataAdapter<Cheese, CheeseViewHolder>(diffCallback) {
class CheeseAdapter : PagingDataAdapter<CheeseListItem, CheeseViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
holder.bindTo(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder =
CheeseViewHolder(parent)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder {
return CheeseViewHolder(parent)
}
companion object {
/**
* This diff callback informs the PagedListAdapter how to compute list differences when new
* PagedLists arrive.
* <p>
*
* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
* detect there's only a single item difference from before, so it only needs to animate and
* rebind a single view.
*
* @see DiffUtil
*/
private val diffCallback = object : DiffUtil.ItemCallback<Cheese>() {
override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem.id == newItem.id
val diffCallback = object : DiffUtil.ItemCallback<CheeseListItem>() {
override fun areItemsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return if (oldItem is CheeseListItem.Item && newItem is CheeseListItem.Item) {
oldItem.cheese.id == newItem.cheese.id
} else if (oldItem is CheeseListItem.Separator && newItem is CheeseListItem.Separator) {
oldItem.name == newItem.name
} else {
oldItem == newItem
}
}
/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return oldItem == newItem
}
}
}
}
......@@ -57,7 +57,6 @@ abstract class CheeseDb : RoomDatabase() {
}
}
private val CHEESE_DATA = arrayListOf(
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
......
package paging.android.example.com.pagingsample
/**
* Common UI model between the [Cheese] data class and separators.
*/
sealed class CheeseListItem(val name: String) {
data class Item(val cheese: Cheese) : CheeseListItem(cheese.name)
data class Separator(private val letter: Char) : CheeseListItem(letter.toUpperCase().toString())
}
\ No newline at end of file
......@@ -16,27 +16,37 @@
package paging.android.example.com.pagingsample
import androidx.recyclerview.widget.RecyclerView
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
/**
* A simple ViewHolder that can bind a Cheese item. It also accepts null items since the data may
* not have been fetched before it is bound.
* A simple ViewHolder that can bind a Cheese or Separator item. It also accepts null items since
* the data may not have been fetched before it is bound.
*/
class CheeseViewHolder(parent :ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false)) {
class CheeseViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false)
) {
var cheese: Cheese? = null
private set
private val nameView = itemView.findViewById<TextView>(R.id.name)
var cheese : Cheese? = null
/**
* Items might be null if they are not paged in yet. PagedListAdapter will re-bind the
* ViewHolder when Item is loaded.
*/
fun bindTo(cheese : Cheese?) {
this.cheese = cheese
nameView.text = cheese?.name
fun bindTo(item: CheeseListItem?) {
if (item is CheeseListItem.Separator) {
nameView.text = "${item.name} Cheeses"
nameView.setTypeface(null, Typeface.BOLD)
} else {
nameView.text = item?.name
nameView.setTypeface(null, Typeface.NORMAL)
}
cheese = (item as? CheeseListItem.Item)?.cheese
nameView.text = item?.name
}
}
\ No newline at end of file
}
......@@ -16,25 +16,23 @@
package paging.android.example.com.pagingsample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* A simple [AndroidViewModel] that provides a [Flow]<[PagingData]> of delicious cheeses.
*/
class CheeseViewModel(app: Application) : AndroidViewModel(app) {
private val dao = CheeseDb.get(app).cheeseDao()
class CheeseViewModel(private val dao: CheeseDao) : ViewModel() {
/**
* We use the Kotlin [Flow] property available on [Pager]. Java developers should use the
* RxJava or LiveData extension properties available in `PagingRx` and `PagingLiveData`.
*/
val allCheeses = Pager(
PagingConfig(
val allCheeses: Flow<PagingData<CheeseListItem>> = Pager(
config = PagingConfig(
/**
* A good page size is a value that fills at least a few screens worth of content on a
* large device so the User is unlikely to see a null item.
......@@ -66,6 +64,30 @@ class CheeseViewModel(app: Application) : AndroidViewModel(app) {
) {
dao.allCheesesByName()
}.flow
.map { pagingData ->
pagingData
// Map cheeses to common UI model.
.map { cheese -> CheeseListItem.Item(cheese) }
.insertSeparators { before: CheeseListItem?, after: CheeseListItem? ->
if (before == null && after == null) {
// List is empty after fully loaded; return null to skip adding separator.
null
} else if (after == null) {
// Footer; return null here to skip adding a footer.
null
} else if (before == null) {
// Header
CheeseListItem.Separator(after.name.first())
} else if (before.name.first() != after.name.first()) {
// Between two items that start with different letters.
CheeseListItem.Separator(after.name.first())
} else {
// Between two items that start with the same letter.
null
}
}
}
.cachedIn(viewModelScope)
fun insert(text: CharSequence) = ioThread {
dao.insert(Cheese(id = 0, name = text.toString()))
......
package paging.android.example.com.pagingsample
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
/**
* A [ViewModelProvider.Factory] that provides dependencies to [CheeseViewModel],
* allowing tests to switch out [CheeseDao] implementation via constructor injection.
*/
class CheeseViewModelFactory(
private val app: Application
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CheeseViewModel::class.java)) {
val cheeseDao = CheeseDb.get(app).cheeseDao()
@Suppress("UNCHECKED_CAST") // Guaranteed to succeed at this point.
return CheeseViewModel(cheeseDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
\ No newline at end of file
......@@ -37,7 +37,7 @@ import paging.android.example.com.pagingsample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
private set
private val viewModel by viewModels<CheeseViewModel>()
private val viewModel by viewModels<CheeseViewModel> { CheeseViewModelFactory(application) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -61,9 +61,17 @@ class MainActivity : AppCompatActivity() {
private fun initSwipeToDelete() {
ItemTouchHelper(object : ItemTouchHelper.Callback() {
// enable the items to swipe to the left or right
override fun getMovementFlags(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder): Int =
makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val cheeseViewHolder = viewHolder as CheeseViewHolder
return if (cheeseViewHolder.cheese != null) {
makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
} else {
makeMovementFlags(0, 0)
}
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean = false
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -87,6 +87,7 @@ dependencies {
androidTestImplementation deps.atsl.runner
androidTestImplementation deps.atsl.rules
androidTestImplementation deps.arch_core.testing
androidTestImplementation deps.espresso.contrib
testImplementation deps.junit
testImplementation deps.coroutines.test
......
......@@ -18,9 +18,12 @@ package com.android.example.paging.pagingwithnetwork.reddit.ui
import android.app.Application
import android.content.Intent
import androidx.test.annotation.UiThreadTest
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.android.example.paging.pagingwithnetwork.R
import com.android.example.paging.pagingwithnetwork.reddit.DefaultServiceLocator
import com.android.example.paging.pagingwithnetwork.reddit.ServiceLocator
import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
......@@ -65,9 +68,8 @@ class RedditActivityTest(private val type: RedditPostRepository.Type) {
}
@Test
@UiThreadTest
fun showSomeResults() {
val scenario = ActivityScenario.launch<RedditActivity>(
ActivityScenario.launch<RedditActivity>(
RedditActivity.intentFor(
context = ApplicationProvider.getApplicationContext(),
type = type
......@@ -76,8 +78,12 @@ class RedditActivityTest(private val type: RedditPostRepository.Type) {
}
)
scenario.onActivity { activity ->
val recyclerView = activity.binding.list
onView(withId(R.id.list)).check { view, noViewFoundException ->
if (noViewFoundException != null) {
throw noViewFoundException
}
val recyclerView = view as RecyclerView
assertEquals(3, recyclerView.adapter?.itemCount)
}
}
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
......@@ -52,7 +52,7 @@ versions.mockito_android = "2.25.0"
versions.mockwebserver = "3.8.1"
versions.navigation = "2.3.0-alpha01"
versions.okhttp_logging_interceptor = "3.9.0"
versions.paging = "3.0.0-alpha13"
versions.paging = "3.0.0-beta01"
versions.recyclerview = "1.2.0-beta01"
versions.retrofit = "2.9.0"
versions.robolectric = "4.2"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册