Android memory usage keeps increasing while using app - java

I have developed a compose app which retrieves crypto data (a list of 1063 objects) from Socket each 3 seconds. While using app I recognized that after some time working with app it crashes with Out of memory. I profiled my app (release version) and recognized that while using app java code increase until it crashes with OOM error.
Let me show you some code for better understanding.
For socket, I created a SocketManager class which instantiates on app startup via Dagger Hilt in singleton component.
class SocketManager #Inject constructor(
socket: Socket,
private val json: Json
) {
var cryptoFlow = MutableStateFlow<List<CryptoDataSetItemDto>>(emptyList())
init {
socket.connect()
socket.on("crypto_data") {
it.forEach { data ->
try {
val coinList =
json.decodeFromString<List<CryptoDataSetItemDto>>(data.toString())
cryptoFlow.tryEmit(coinList)
} catch (e: Exception) {
Sentry.captureException(e)
}
}
}
}
}
#Module
#InstallIn(SingletonComponent::class)
object SocketModule {
#Provides
#Singleton
fun provideJson(): Json {
return Json {
encodeDefaults = true
ignoreUnknownKeys = true
}
}
#Provides
#Singleton
fun provideSocket(client: OkHttpClient): Socket {
val socket: Socket
val opts = IO.Options()
opts.path = Constants.SOCKET_PATH
opts.secure = true
socket = IO.socket(Constants.SOCKET_BASE_URL, opts)
return socket
}
#Provides
#Singleton
fun provideSocketManager(socket: Socket, json: Json): SocketManager {
return SocketManager(socket, json)
}
}
Then I inject SocketManager to viewModel to get data on the screens I need those data. I create a function which gets a coroutine context from composable and collects data in that scope.
#HiltViewModel
class WithdrawDepositViewModel #Inject constructor(
private val useCase: WalletUseCases,
private val socketManager: SocketManager,
savedStateHandle: SavedStateHandle
) : ViewModel() {
fun getCoinData(context: CoroutineContext) {
CoroutineScope(context).launch(Dispatchers.Default) {
socketManager.cryptoFlow.asStateFlow()
.shareIn(CoroutineScope(context), SharingStarted.WhileSubscribed())
.collectLatest { cryptoListDto ->
val cryptoList = cryptoListDto.map { it.toCryptoList() }.toMutableList()
val irt = CryptoDataItem(
enName = "Toman",
faName = "تومان",
symbol = "IRT"
)
cryptoList.add(0, irt)
_state.value = state.value.copy(
cryptoList = cryptoList,
isLoading = false
)
}
}
}
and in screen I call the function like this:
#Composable
fun HomeScreen(
onNavigate: (String) -> Unit,
scaffoldState: ScaffoldState,
viewModel: HomeScreenViewModel = hiltViewModel()
) {
val scope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
viewModel.getCryptoData(scope.coroutineContext)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
This is my architecture to get data. when looked at heap dump result, I recognized that CryptoDataSetItemDto object which I get it on socket, has allocated lots of memory. I can not find the problem also I know that there is better architecture which I hope to learn from you.

Related

Switching between sender and receiver in Android Wi-Fi Direct app

I am developing an Android app to share files using Wi-Fi Direct. The app works, but I am unable to switch between being a sender and a receiver without closing the app. When I try to switch roles, the group owner is not updated and remains the same as the previous transfer. How can I update the group owner and switch roles correctly?
I do disconnect and reinitialize after every file transfer session. But it doesn't create a new group.
Here is my initialization code:
manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
channel = manager?.initialize(this, mainLooper, null)?.also {
receiver = P2pBroadcastReceiver(this)
}
And here is the code for connecting to a peer and switching roles:
fun connectToPeer(device: WifiP2pDevice, isSender: Boolean) {
val config = WifiP2pConfig().apply {
deviceAddress = device.deviceAddress
groupOwnerIntent = if (isSender) WifiP2pConfig.GROUP_OWNER_INTENT_MIN else WifiP2pConfig.GROUP_OWNER_INTENT_MAX
}
channel?.also { channel ->
manager?.connect(channel, config, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
context.apply {
toast("${getString(R.string.connecting_to)} ${device.deviceName}")
}
}
override fun onFailure(reasonCode: Int) = onP2POperationFailed(reasonCode)
})
}
}
And here is the code for disconnecting from the group:
fun disconnect() {
manager?.apply {
cancelConnect(channel, null)
removeGroup(channel, null)
stopPeerDiscovery(channel, null)
deletePersistentGroups()
}
try {
if (::serverSocket.isInitialized) serverSocket.close()
if (::clientSocket.isInitialized) clientSocket.close()
} catch (e: IOException) {}
}
private fun deletePersistentGroups() {
try {
val methods = WifiP2pManager::class.java.methods
for (method in methods) {
if (method.name == "deletePersistentGroup") {
for (networkId in 0..31) {
method.invoke(manager, channel, networkId, null)
}
}
}
} catch (e: Exception) {}
}
Any help would be greatly appreciated. Thank you!
I would like for devices d1 and d2 to have the ability to switch roles without closing the app. Currently, d1 is set up to receive files and d2 is set up to send them. However, if d1 needs to send files without closing the app after d2 has finished sending its files, I would like for them to be able to easily reestablish the connection and initiate the transfer.
Edit 2:
How I initiate the file transferring or sending process:
private fun onConnectionInfoChanged(info: WifiP2pInfo?) {
if (isDeviceConnected) {
if (info!!.isGroupOwner) { // receiver || server || group owner
startReceiverServer()
} else { // sender || client
startSenderClient(info.groupOwnerAddress)
}
}
}
private fun startReceiverServer() {
runOnBg {
serverSocket = ServerSocket(P2pFileTransferHelper.PORT)
P2pFileTransferHelper.receiveFiles(serverSocket, context.contentResolver) { fileTransferInfoLiveData.postValue(it) }
}
}
private fun startSenderClient(serverAddress: InetAddress) {
runOnBg {
clientSocket = Socket()
P2pFileTransferHelper.sendFiles(clientSocket, serverAddress, selectedFilePaths) { fileTransferInfoLiveData.postValue(it) }
}
}

I have a Contract deployed, a project created on infura and a metamask wallet. How do I bundle all this to handle contract interactions?

I have my contract deployed, my infura project ready and my metamask testnet wallet [0xE4e609e2E928E8F8b74C6Bb37e13503b337f8C70] ready with 0.99 eth how do I use all this to interact with my contract?
I have written some java code till now using references from a project I found. but, the project didn't make use of the wallet for the transactions. and that's what confused me. And obviously the code didn't run.
here's the code I wrote with the intent to interact with the contract
package com.kenetic.blockchainvs.block_connector.contract.contract_interface
import org.web3j.abi.TypeReference
import org.web3j.abi.datatypes.*
import org.web3j.abi.datatypes.Array
import org.web3j.abi.datatypes.Function
import org.web3j.abi.datatypes.generated.Uint256
import org.web3j.protocol.Web3j
import org.web3j.protocol.core.RemoteCall
import org.web3j.protocol.core.methods.response.TransactionReceipt
import org.web3j.tx.Contract
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
// TODO: check values for binary and addresses
private const val CONTRACT_BINARY = "608060405234801561001057600080fd5b506000600181905550600060028190555060006003819055506106ea806100386000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c80630efcb43c1461006757806342cff73814610085578063449b6db2146100a3578063464d4a92146100c1578063a483f4e1146100dd578063f5a31579146100fb575b600080fd5b61006f610119565b60405161007c91906104f5565b60405180910390f35b61008d610123565b60405161009a9190610498565b60405180910390f35b6100ab6101b1565b6040516100b891906104ba565b60405180910390f35b6100db60048036038101906100d691906103a5565b610261565b005b6100e561037c565b6040516100f291906104f5565b60405180910390f35b610103610386565b60405161011091906104f5565b60405180910390f35b6000600354905090565b606060008054806020026020016040519081016040528092919081815260200182805480156101a757602002820191906000526020600020905b8160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001906001019080831161015d575b5050505050905090565b60008033905060005b60008054905081101561025757600081815481106101db576101da61061a565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156102445760019250505061025e565b808061024f906105a2565b9150506101ba565b6000925050505b90565b6004811080156102715750600081115b6102b0576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a7906104d5565b60405180910390fd5b6000339080600181540180825580915050600190039060005260206000200160009091909190916101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506001811415610339576001600081548092919061032f906105a2565b9190505550610379565b600281141561035f5760026000815480929190610355906105a2565b9190505550610378565b60036000815480929190610372906105a2565b91905055505b5b50565b6000600254905090565b6000600154905090565b60008135905061039f8161069d565b92915050565b6000602082840312156103bb576103ba610649565b5b60006103c984828501610390565b91505092915050565b60006103de83836103ea565b60208301905092915050565b6103f38161055a565b82525050565b600061040482610520565b61040e8185610538565b935061041983610510565b8060005b8381101561044a57815161043188826103d2565b975061043c8361052b565b92505060018101905061041d565b5085935050505092915050565b6104608161056c565b82525050565b6000610473603983610549565b915061047e8261064e565b604082019050919050565b61049281610598565b82525050565b600060208201905081810360008301526104b281846103f9565b905092915050565b60006020820190506104cf6000830184610457565b92915050565b600060208201905081810360008301526104ee81610466565b9050919050565b600060208201905061050a6000830184610489565b92915050565b6000819050602082019050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600061056582610578565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b60006105ad82610598565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156105e0576105df6105eb565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600080fd5b7f74686520676976656e206e756d62657220697320696e76616c6964206173207460008201527f6865206e756d626572206973206f7574206f662072616e676500000000000000602082015250565b6106a681610598565b81146106b157600080fd5b5056fea26469706673582212205da625bda0850f1ae09940b191c98f12fd976f993abe0e3ed1fa777c406ebf0564736f6c63430008070033"
private const val CONTRACT_ADDRESS = "0xe2d8a60415adaa2Ba87c44b8C20C9A15e3F9178a"
class VoteContractAccessor : Contract {
//
private val functionGetHasAlreadyVoted = "hasAlreadyVoted"
//----------------------------------------------------------------------------------casting-vote
private val functionRegisterVote = "registerVote"
//--------------------------------------------------------------------------------view-functions
private val functionGetAddressValues = "getAddressValues"
private val functionGetPartyOneVotes = "getParty1Votes"
private val functionGetPartyTwoVotes = "getParty2Votes"
private val functionGetPartyThreeVotes = "getParty3Votes"
constructor(
web3j: Web3j,
transactionManager: TransactionManager,
gasProvider: ContractGasProvider
) : super(CONTRACT_BINARY, CONTRACT_ADDRESS, web3j, transactionManager, gasProvider)
lateinit var a: Array<Address>
fun getAddressValues(): RemoteCall<Array<Address>> {
val function = Function(
functionGetAddressValues,
listOf(),
listOf<TypeReference<*>>(object : TypeReference<Array<Address>>() {})
)
// TODO: check for java-class error and null pointer error
return executeRemoteCallSingleValueReturn(function, a.javaClass)
}
fun getPartyVotes(party: PartyEnum): RemoteCall<Uint256> {
val function = Function(
when (party) {
PartyEnum.ONE -> {
functionGetPartyOneVotes
}
PartyEnum.TWO -> {
functionGetPartyTwoVotes
}
PartyEnum.THREE -> {
functionGetPartyThreeVotes
}
},
listOf(),
listOf<TypeReference<*>>(object : TypeReference<Uint256>() {})
)
return executeRemoteCallSingleValueReturn(function, Uint256::class.java)
}
fun putVote(party: PartyEnum): RemoteCall<TransactionReceipt> {
val function = Function(
functionRegisterVote,
listOf<Type<*>>(
Uint256(
when (party) {
PartyEnum.ONE -> 1
PartyEnum.TWO -> 2
PartyEnum.THREE -> 3
}
)
),
emptyList()
)
return executeRemoteCallTransaction(function)
}
fun getHasAlreadyVoted():RemoteCall<Bool>{
val function = Function(
functionGetHasAlreadyVoted,
listOf(),
listOf<TypeReference<*>>(object : TypeReference<Bool>() {})
)
return executeRemoteCallSingleValueReturn(function, Bool::class.java)
}
}
And here's the code I use to access the above functions -
package com.kenetic.blockchainvs.block_connector.contract.contract_interface
import android.util.Log
import org.web3j.abi.datatypes.generated.Uint256
import org.web3j.crypto.Credentials
import org.web3j.protocol.Web3j
import org.web3j.protocol.http.HttpService
import org.web3j.tx.RawTransactionManager
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
import org.web3j.tx.gas.DefaultGasProvider
import java.math.BigInteger
import java.util.concurrent.TimeUnit
private const val TAG = "VoteContractDelegate"
class VoteContractDelegate() {
//-----------------------------------------------------------------------------contract-elements
// TODO: check these values
private val MINIMUM_GAS_LIMIT = 30000
private val MAX_GAS_LIMIT: BigInteger = BigInteger.valueOf(3000000)//-------------self-added
private val PRIVATE_KEY_ROPSTEN = "c9852fcf061b47c58d5294cd7a23548c"
private val ROPSTEN_INFURA_URL = "https://ropsten.infura.io/v3/c358089e1aaa4746aa50e61d4ec41c5c"
// private val credentials = WalletUtils.loadCredentials("qISTALO-42", "0xE4e609e2E928E8F8b74C6Bb37e13503b337f8C70")
private val credentials = Credentials.create(PRIVATE_KEY_ROPSTEN)
private lateinit var web3j: Web3j//--------------------------------------------works-as-intended
private lateinit var contract: VoteContractAccessor
private lateinit var transactionManager: TransactionManager
private val gasProvider: ContractGasProvider = DefaultGasProvider()
init {
instantiateWeb3J()
transactionManager = RawTransactionManager(web3j, credentials)
initializeContract()
}
private fun instantiateWeb3J() {// TODO: first
try {
web3j = Web3j.build(HttpService(ROPSTEN_INFURA_URL))
Log.d(TAG, "Connection Successful")
} catch (e: Exception) {
e.printStackTrace()
Log.d(TAG, "Connection Unsuccessful, error : ${e.message}")
}
}
private fun initializeContract() {// TODO: second
try {
contract = VoteContractAccessor(web3j, transactionManager, gasProvider)
Log.d(TAG, "Contract Initialized Successfully")
} catch (e: Exception) {
Log.d(TAG, "Contract Initialization Error")
}
}
//------------------------------------------------------------------------visual-type-check-done
fun partyVotesStatus(): String {
return try {
// TODO: get values of votes and add them to the string
val votesForOne: Uint256 = contract.getPartyVotes(PartyEnum.ONE).sendAsync().get()
val votesForTwo: Uint256 = contract.getPartyVotes(PartyEnum.TWO).sendAsync().get()
val votesForThree: Uint256 = contract.getPartyVotes(PartyEnum.THREE).sendAsync().get()
"party votes status: " +
"\nparty one votes = " + votesForOne +
"\nparty one votes = " + votesForTwo +
"\nparty one votes = " + votesForThree
} catch (e: Exception) {
e.printStackTrace()
"error has occurred, ${e.message}"
}
}
//------------------------------------------------------------------------visual-type-check-done
fun casteVote(party: PartyEnum): Int {
return try {
contract
.putVote(party)
.sendAsync()
.get(3, TimeUnit.MINUTES)
.gasUsed
.toInt()
} catch (e: java.lang.Exception) {
e.printStackTrace()
0
}
}
// TODO: change set return type to array
fun getVoterAddresses(): String {
return try {
contract.getAddressValues().sendAsync().get().typeAsString
} catch (e: Exception) {
e.printStackTrace()
"Error has occurred while receiving address list - ${e.message}"
}
}
fun getHasAlreadyVoted(): Boolean {
// TODO: change this
return try {
contract.getHasAlreadyVoted().sendAsync().get().value
} catch (e: Exception) {
// TODO: check if to change according to error
true
}
}
//checking connection for testing
fun abc() {
val web3: Web3j =
Web3j.build(HttpService("https://ropsten.infura.io/v3/c358089e1aaa4746aa50e61d4ec41c5c"));
try {
val clientVersion = web3.web3ClientVersion().sendAsync().get();
if (clientVersion.hasError()) {
Log.d(TAG, "web3 Connection successful with error")
} else {
Log.d(TAG, "web3 Connection successful without error")
}
} catch (e: Exception) {
Log.d(TAG, "web3 Connection unsuccessful")
}
}
}
Just note I am a total beginner at this. So, I would probably have made some weird mistakes. also, I have included the private keys which I know I shouldn't. but, it's for a college project so, not that big a deal. I just thought It would clear out any confusions about that being a problem in the code.
any help would be appriciated, If you have a sample code that you think is similiar to mine but actually works, it might be extremely helpful as I can't find that many example codes which don't confuse me. Don't bother with the language, aslong as it is understandable (uses english words in the syntax), i can make use of it.

Reflection and Invoking Method from Java to Kotlin

I'm trying to convert the following reflection into Kotlin. The following uses reflection to call an RFCOMMs function so it can take a port/channel as an input instead of UUID. I have all my program in Kotlin. Anyone know how to write this in Kotlin?
int bt_port_to_connect = 5;
BluetoothDevice device = mDevice;
BluetoothSocket deviceSocket = null;
...
// IMPORTANT: we create a reference to the 'createInsecureRfcommSocket' method
// and not(!) to the 'createInsecureRfcommSocketToServiceRecord' (which is what the
// android SDK documentation publishes
Method m = device.getClass().getMethod("createInsecureRfcommSocket", new Class[] {int.class});
deviceSocket = (BluetoothSocket) m.invoke(device,bt_port_to_connect);
Updating with recommendation:
class BluetoothClient(device: BluetoothDevice): Thread() {
// https://stackoverflow.com/questions/9703779/connecting-to-a-specific-bluetooth-port-on-a-bluetooth-device-using-android
// Need to reflection - create RFCOMM socket to a port number instead of UUID
// Invoke btdevice as 1st parameter and then the port number
var bt_port_to_connect = 5
var deviceSocket: BluetoothSocket? = null
private val socket = device.createInsecureRfcommSocketToServiceRecord(uuid)
val m = device::class.declaredFunctions.single { it.name == "createInsecureRfcommSocket" }
m.call(device, bt_port_to_connect)
override fun run() {
try {
Log.i("client", "Connecting")
this.socket.connect()
Log.i("client", "Sending")
val outputStream = this.socket.outputStream
val inputStream = this.socket.inputStream
try {
outputStream.write(message.toByteArray())
outputStream.flush()
Log.i("client", "Sent")
} catch(e: Exception) {
Log.e("client", "Cannot send", e)
} finally {
outputStream.close()
inputStream.close()
this.socket.close()
}
}
catch (e: IOException) {
println("Socket Failed")
}
}
}
You can really use exactly the same code, just convert it to Kotlin:
val m = device::class.java.getMethod("createInsecureRfcommSocket", Int::class.java)
m.invoke(device, bt_port_to_connect)
Or you can use Kotlin reflection:
val m = device::class.declaredFunctions.single { it.name == "createInsecureRfcommSocket" }
m.call(device, bt_port_to_connect)
I don't know if there is any better way to find a function with provided name. You can create an extension function to make it cleaner. You may also need to check parameters if this function has overrides.

How can I return one list from Rxjava instead of multiple emitted singles?

I have a call to google place autocomplete sdk on a Rxjava that brings me a list of AutoCompletePredictions and then I use that list to iterate and call the place details sdk of google with that I want to return a single list with all the places details but it doesnt triggers.
fun searchAutoComplete(word: String): Single<MutableList<SuggestedPlace>> {
if (placeClient == null) {
placeClient = this.context?.let { Places.createClient(it) }
}
return Observable.create<SuggestedPlace> { emiter ->
var request = GooglePlaceHelper.getPlacesSuggestions(word)
placeClient?.findAutocompletePredictions(request)
?.addOnSuccessListener { response: FindAutocompletePredictionsResponse ->
response.autocompletePredictions.forEach { place ->
var request = GooglePlaceHelper.getPlaceDetailRequest(place.placeId)
placeClient?.fetchPlace(request)
?.addOnSuccessListener { response: FetchPlaceResponse ->
val place = response.place
place?.let {
var suggestedPlace = SuggestedPlace(place.address!!, place.latLng?.latitude!!, place.latLng?.longitude!!)
emiter.onNext(suggestedPlace)
}
}
}
}
}.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()).toList()
}
Please provide a proper example next time. It is quite work intensive to mock your APIs.
import io.reactivex.rxjava3.core.Single
import org.junit.jupiter.api.Test
This example will wrap the GoogleApi into a reactive-API, which provides Single functions. In order to collect all results in a List, you could use Single.zip.
Note: You should not use MutableList with RxJava. Always use immutable data types, or you get into trouble.
class So65684080 {
#Test
fun so65684080() {
val googleClientStub = GoogleClientStub()
val reactiveClient = GoogleClientReactiveImpl(googleClientStub)
val searchApiImpl = SearchApiImpl(reactiveClient)
searchApiImpl.search("whatever")
.test()
.assertValue(listOf(SuggestedPlace("fetchPlace"), SuggestedPlace("fetchPlace")))
}
}
internal interface SearchApi {
fun search(word: String): Single<List<SuggestedPlace>>
}
internal class SearchApiImpl(private val client: GoogleClientReactive) : SearchApi {
override fun search(word: String): Single<List<SuggestedPlace>> {
return client.findAutocompletePredictions("whatever")
.flatMap { resp ->
val fetches = resp.values.map { r -> client.fetchPlace(r) }
Single.zip(fetches) { arr ->
arr.map {
val fetchPlaceResponse = it as FetchPlaceResponse
SuggestedPlace(fetchPlaceResponse.name)
}
.toList()
}
}
}
}
internal interface GoogleClient {
fun findAutocompletePredictions(request: String): Result<FindAutocompletePredictionsResponse>
fun fetchPlace(request: String): Result<FetchPlaceResponse>
}
internal interface GoogleClientReactive {
fun findAutocompletePredictions(request: String): Single<FindAutocompletePredictionsResponse>
fun fetchPlace(request: String): Single<FetchPlaceResponse>
}
internal class GoogleClientStub : GoogleClient {
override fun findAutocompletePredictions(request: String): Result<FindAutocompletePredictionsResponse> {
return ResultStub<FindAutocompletePredictionsResponse>(FindAutocompletePredictionsResponse(listOf("fetch1", "fetch2")))
}
override fun fetchPlace(request: String): Result<FetchPlaceResponse> {
return ResultStub<FetchPlaceResponse>(FetchPlaceResponse("fetchPlace"))
}
}
internal class GoogleClientReactiveImpl(private val client: GoogleClient) : GoogleClientReactive {
override fun findAutocompletePredictions(request: String): Single<FindAutocompletePredictionsResponse> {
return Single.create { emitter ->
val response: (FindAutocompletePredictionsResponse) -> Unit = {
emitter.onSuccess(it)
}
client.findAutocompletePredictions(request).addOnSuccessListener(response)
// TODO: set emitter.setCancellable {} for unsubscribing
}
}
override fun fetchPlace(request: String): Single<FetchPlaceResponse> {
return Single.create { emitter ->
val response: (FetchPlaceResponse) -> Unit = {
emitter.onSuccess(it)
}
client.fetchPlace(request).addOnSuccessListener(response)
// TODO: set emitter.setCancellable {} for unsubscribing
}
}
}
internal data class SuggestedPlace(val name: String)
internal data class FetchPlaceResponse(val name: String)
internal data class FindAutocompletePredictionsResponse(val values: List<String>)
internal interface Result<T> {
fun addOnSuccessListener(response: (r: T) -> Unit)
}
internal class ResultStub<T>(val value: T) : Result<T> {
override fun addOnSuccessListener(response: (r: T) -> Unit) {
response(value)
}
}
Note
I did not add observeOn and subscribeOn, because it makes testing a little more difficulty. Please add it by yourself, at the end of the Single form SearchApiImpl#search

How to handle mocked RxJava2 observable throwing exception in unit test

I have been doing TDD in Kotlin for these past few weeks now in Android using MVP. Things have been going well.
I use Mockito to mock classes but I can't seem to get over on how to implement one of the tests I wanted to run.
The following are my tests:
Call api, receive list of data, then show list. loadAllPlacesTest()
Call api, receive empty data, then show list. loadEmptyPlacesTest()
Call api, some exception happen on the way, then show error message. loadExceptionPlacesTest()
I have tests for #1 and #2 successfully. The problem is with #3, I'm not sure how to approach the test in code.
RestApiInterface.kt
interface RestApiInterface {
#GET(RestApiManager.PLACES_URL)
fun getPlacesPagedObservable(
#Header("header_access_token") accessToken: String?,
#Query("page") page: Int?
): Observable<PlacesWrapper>
}
RestApiManager.kt
the manager class implementing the interface looks like this:
open class RestApiManager: RestApiInterface{
var api: RestApiInterface
internal set
internal var retrofit: Retrofit
init {
val logging = HttpLoggingInterceptor()
// set your desired log level
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = okhttp3.OkHttpClient().newBuilder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.build()
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//very important for RXJAVA and retrofit
.build()
api = retrofit.create(RestApiInterface::class.java)
}
override fun getPlacesPagedObservable(accessToken: String?, page: Int?): Observable<PlacesWrapper> {
//return throw Exception("sorry2")
return api.getPlacesPagedObservable(
accessToken,
page)
}
}
}
Here is my unit test:
class PlacesPresenterImplTest : AndroidTest(){
lateinit var presenter:PlacesPresenterImpl
lateinit var view:PlacesView
lateinit var apiManager:RestApiManager
//lateinit var apiManager:RestApiManager
val EXCEPTION_MESSAGE1 = "SORRY"
val MANY_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
var EXCEPTION_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
val manyPlacesWrapper = PlacesWrapper(MANY_PLACES)
var exceptionPlacesWrapper = PlacesWrapper(EXCEPTION_PLACES)
val emptyPlacesWrapper = PlacesWrapper(Collections.emptyList())
#After
fun clear(){
RxJavaPlugins.reset()
}
#Before
fun init(){
//MOCKS THE subscribeOn(Schedulers.io()) to use the same thread the test is being run on
//Schedulers.trampoline() runs the test in the same thread used by the test
RxJavaPlugins.setIoSchedulerHandler { t -> Schedulers.trampoline() }
view = Mockito.mock<PlacesView>(PlacesView::class.java)
apiManager = Mockito.mock(RestApiManager::class.java)
presenter = PlacesPresenterImpl(view,context(), Bundle(), Schedulers.trampoline())
presenter.apiManager = apiManager
//exceptionPlacesWrapper = throw Exception(EXCEPTION_MESSAGE1);
}
#Test
fun loadAllPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(manyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadEmptyPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(emptyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadExceptionPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenThrow(Exception(EXCEPTION_MESSAGE1))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.never())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(EXCEPTION_MESSAGE1)
}
}
PlacesPresenterImpl.kt
This is the presenter.
class PlacesPresenterImpl
constructor(var view: PlacesView, var context: Context, var savedInstanceState:Bundle?, var mainThread: Scheduler)
: BasePresenter(), BasePresenterInterface, PlacesPresenterInterface {
lateinit var apiManager:RestApiInterface
var placeListRequest: Disposable? = null
override fun __firstInit() {
apiManager = RestApiManager()
}
override fun __init(context: Context, savedInstanceState: Bundle, view: BaseView?) {
this.view = view as PlacesView
if (__isFirstTimeLoad())
__firstInit()
}
override fun __destroy() {
placeListRequest?.dispose()
}
override fun __populate() {
_callPlacesApi()
}
override fun _callPlacesApi() {
view.__showLoading()
apiManager.getPlacesPagedObservable("", 0)
.subscribeOn(Schedulers.io())
.observeOn(mainThread)
.subscribe (object : DisposableObserver<PlacesWrapper>() {
override fun onNext(placesWrapper: PlacesWrapper) {
placesWrapper?.let {
val size = placesWrapper.place?.size
view.__hideLoading()
view._showList()
System.out.println("Great I found " + size + " records of places.")
view.__showFullScreenMessage("Great I found " + size + " records of places.")
}
System.out.println("onNext()")
}
override fun onError(e: Throwable) {
System.out.println("onError()")
//e.printStackTrace()
view.__hideLoading()
if (ExceptionsUtil.isNoNetworkException(e)){
view.__showFullScreenMessage("So sad, can not connect to network to get place list.")
}else{
view.__showFullScreenMessage("Oops, something went wrong. ["+e.localizedMessage+"]")
}
this.dispose()
}
override fun onComplete() {
this.dispose()
//System.out.printf("onComplete()")
}
})
}
private fun _getEventCompletionObserver(): DisposableObserver<String> {
return object : DisposableObserver<String>() {
override fun onNext(taskType: String) {
//_log(String.format("onNext %s task", taskType))
}
override fun onError(e: Throwable) {
//_log(String.format("Dang a task timeout"))
//Timber.e(e, "Timeout Demo exception")
}
override fun onComplete() {
//_log(String.format("task was completed"))
}
}
}}
Problem/Questions for the loadExceptionPlacesTest()
I'm not sure why the code doesn't go to the Presenter's onError().
correct me if I'm wrong the following but this is what I think:
a - `apiManager.getPlacesPagedObservable("", 0)` observable itself throws an Exception that is why the `.subscribe()` can not happen/proceed and the methods of the observer won't get called,
b - it will only go to onError() when the operations inside the observable encounters an Exception like JSONException
For loadExceptionPlacesTest() I think the 1b above is the way to go to make the presenter's onError() get called and make the test pass. Is this correct? If it is how to do it on the test. If it is not can you guys point out what I am missing or doing wrong?
I'll leave this here for future reference and to be able to elaborate a bit more, even though I've answered in the comments.
What you're trying to accomplish is to put the stream in the onError flow. Unfortunately, by mocking it like this:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenThrow(Exception(EXCEPTION_MESSAGE1))
You're actually telling Mockito to setup your mock in a way that just calling apiManager.getPlacesPagedObservable(anystring, anystring) should thrown an exception.
It is indeed true that throwing an exception inside an Rx stream will cause the entire stream to stop and end up in the onError method. However, this is exactly the problem with the approach you're using. You're not inside the stream when the exception is thrown.
Instead what you want to do is tell Mockito that once you call apiManager.getPlacesPagedObservable(anystring, anystring) you want to return a stream that will end up in the onError. This can be easily achieved with Observable.error() like so:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.a‌​nyString(), Mockito.anyInt()))
.thenReturn(Observable.error(
Exception(EXCEPTION_MESSAGE1)))
(It might be possible that you need to add some type information in this part here Observable.error(), you might also need to use something else instead of an observable - single, completable, etc.)
The mocking above will tell Mockito to setup your mock to return an observable that will error as soon as it's subscribed to. This will in turn put your subscriber directly in the onError stream with the specified exception.
Below is an example of a Test that invoke a REST service through Repository from a ViewModel according to the MVVM pattern.
The REST service returns an Exception, here is the test case:
#RunWith(AndroidJUnit4::class)
class StargazersViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Subject under test
private lateinit var viewModel: MyViewModel
#Mock
private lateinit var repositoryMock: MyRepository
#Before
fun setup() {
MockitoAnnotations.openMocks(this)
val appContext = ApplicationProvider.getApplicationContext<Application>()
viewModel = MyViewModel(repositoryMock, appContext)
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
}
#Test
fun `invoke rest with failure`() {
whenever(
repositoryMock.loadDataSingle(Mockito.anyString(), Mockito.anyInt())
).thenAnswer {
Single.error<retrofit2.HttpException>(
retrofit2.HttpException(
Response.error<String>(
404,
"Response.error()".toResponseBody("text/plain; charset=utf-8".toMediaType())
)
)
)
}
}
}

Categories

Resources