introduce KotlinResultCallAdapter for nice suspending network calls (#2415)
* introduce KotlinResultCallAdapter for nice suspending network calls * fix tests
This commit is contained in:
parent
d21d045eda
commit
3e8c6a318a
15 changed files with 168 additions and 179 deletions
|
@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
}
|
||||
|
||||
private fun fetchUserInfo() {
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
private fun fetchUserInfo() = lifecycleScope.launch {
|
||||
mastodonApi.accountVerifyCredentials().fold(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchUserInfoSuccess(me: Account) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
|
|||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
|
@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
rxSingle {
|
||||
mastodonApi.getInstance().getOrThrow()
|
||||
}.map { Either.Right(it) }
|
||||
}
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
|
|
|
@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor(
|
|||
init {
|
||||
|
||||
Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance()
|
||||
api.getCustomEmojis(),
|
||||
rxSingle {
|
||||
api.getInstance().getOrThrow()
|
||||
}
|
||||
) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
|
@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor(
|
|||
): LiveData<Unit> {
|
||||
|
||||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
||||
} else {
|
||||
Observable.just(Unit)
|
||||
}.toLiveData()
|
||||
|
|
|
@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import com.keylesspalace.tusky.util.rickRoll
|
||||
|
@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
setLoading(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val credentials: AppCredentials = try {
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
).fold(
|
||||
{ credentials ->
|
||||
// Before we open browser page we save the data.
|
||||
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||
// and we will need to pick up the process where we left off.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
// Before we open browser page we save the data.
|
||||
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||
// and we will need to pick up the process where we left off.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
},
|
||||
{ e ->
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
setLoading(true)
|
||||
|
||||
val accessToken = try {
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code,
|
||||
"authorization_code"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(
|
||||
TAG,
|
||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||
)
|
||||
return
|
||||
}
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||
).fold(
|
||||
{ accessToken ->
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||
},
|
||||
{ e ->
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(
|
||||
TAG,
|
||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setLoading(loadingState: Boolean) {
|
||||
|
|
|
@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScheduledStatusViewModel @Inject constructor(
|
||||
|
@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
|
|||
|
||||
fun deleteScheduledStatus(status: ScheduledStatus) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
mastodonApi.deleteScheduledStatus(status.id).await()
|
||||
pagingSourceFactory.remove(status)
|
||||
} catch (throwable: Throwable) {
|
||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||
}
|
||||
mastodonApi.deleteScheduledStatus(status.id).fold(
|
||||
{
|
||||
pagingSourceFactory.remove(status)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.text.Spanned
|
||||
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
|
@ -111,6 +112,7 @@ class NetworkModule {
|
|||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ interface MastodonApi {
|
|||
fun getCustomEmojis(): Single<List<Emoji>>
|
||||
|
||||
@GET("api/v1/instance")
|
||||
fun getInstance(): Single<Instance>
|
||||
suspend fun getInstance(): Result<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
fun getFilters(): Single<List<Filter>>
|
||||
|
@ -249,12 +249,12 @@ interface MastodonApi {
|
|||
): Single<List<ScheduledStatus>>
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
fun deleteScheduledStatus(
|
||||
suspend fun deleteScheduledStatus(
|
||||
@Path("id") scheduledStatusId: String
|
||||
): Single<ResponseBody>
|
||||
): Result<ResponseBody>
|
||||
|
||||
@GET("api/v1/accounts/verify_credentials")
|
||||
fun accountVerifyCredentials(): Single<Account>
|
||||
suspend fun accountVerifyCredentials(): Result<Account>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
|
@ -265,7 +265,7 @@ interface MastodonApi {
|
|||
|
||||
@Multipart
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
fun accountUpdateCredentials(
|
||||
suspend fun accountUpdateCredentials(
|
||||
@Part(value = "display_name") displayName: RequestBody?,
|
||||
@Part(value = "note") note: RequestBody?,
|
||||
@Part(value = "locked") locked: RequestBody?,
|
||||
|
@ -279,7 +279,7 @@ interface MastodonApi {
|
|||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||
): Call<Account>
|
||||
): Result<Account>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
|
@ -447,7 +447,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uris") redirectUris: String,
|
||||
@Field("scopes") scopes: String,
|
||||
@Field("website") website: String
|
||||
): AppCredentials
|
||||
): Result<AppCredentials>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
|
@ -458,7 +458,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uri") redirectUri: String,
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grantType: String
|
||||
): AccessToken
|
||||
): Result<AccessToken>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
|||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
|
@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading
|
|||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
|||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
|
|||
|
||||
private var oldProfileData: Account? = null
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
fun obtainProfile() {
|
||||
fun obtainProfile() = viewModelScope.launch {
|
||||
if (profileData.value == null || profileData.value is Error) {
|
||||
|
||||
profileData.postValue(Loading())
|
||||
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.subscribe(
|
||||
{ profile ->
|
||||
oldProfileData = profile
|
||||
profileData.postValue(Success(profile))
|
||||
},
|
||||
{
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposables)
|
||||
mastodonApi.accountVerifyCredentials().fold(
|
||||
{ profile ->
|
||||
oldProfileData = profile
|
||||
profileData.postValue(Success(profile))
|
||||
},
|
||||
{
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
val newProfileData = response.body()
|
||||
if (!response.isSuccessful || newProfileData == null) {
|
||||
val errorResponse = response.errorBody()?.string()
|
||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||
try {
|
||||
JSONObject(errorResponse).optString("error", null)
|
||||
} catch (e: JSONException) {
|
||||
viewModelScope.launch {
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).fold(
|
||||
{ newProfileData ->
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
},
|
||||
{ throwable ->
|
||||
if (throwable is HttpException) {
|
||||
val errorResponse = throwable.response()?.errorBody()?.string()
|
||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||
try {
|
||||
JSONObject(errorResponse).optString("error", "")
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
saveData.postValue(Error(errorMessage = errorMsg))
|
||||
} else {
|
||||
null
|
||||
saveData.postValue(Error())
|
||||
}
|
||||
saveData.postValue(Error(errorMessage = errorMsg))
|
||||
return
|
||||
}
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
saveData.postValue(Error())
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// cache activity state for rotation change
|
||||
|
@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
|
|||
return File(application.cacheDir, filename)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun obtainInstance() {
|
||||
fun obtainInstance() = viewModelScope.launch {
|
||||
if (instanceData.value == null || instanceData.value is Error) {
|
||||
instanceData.postValue(Loading())
|
||||
|
||||
mastodonApi.getInstance().subscribe(
|
||||
mastodonApi.getInstance().fold(
|
||||
{ instance ->
|
||||
instanceData.postValue(Success(instance))
|
||||
},
|
||||
|
@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
|
|||
instanceData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue