Заставьте Android MVVM, Kotlin Coroutines и Retrofit 2.6 работать асинхронно - PullRequest
0 голосов
/ 20 сентября 2019

Я только что закончил свое первое приложение для Android.Он работает как надо, но, как вы можете себе представить, много спагетти-кода и нехватка производительности.Из того, что я узнал на Android и языке Kotlin, создающих этот проект (и множество статей / учебников / SO ответов), я пытаюсь начать его заново с нуля, чтобы реализовать лучшую версию.На данный момент я хотел бы сделать это как можно более простым, просто чтобы лучше понять, как обрабатывать вызовы API с помощью Retrofit и шаблона MVVM, чтобы не было Volley / RXjava / Dagger и т. Д.

Я начинаю слогин очевидно;Я хотел бы сделать запрос на публикацию, чтобы просто сравнить учетные данные, дождаться ответа и, если он будет положительным, показать «экран загрузки» во время выборки и обработки данных для отображения на домашней странице.Я не храню никакой информации, поэтому я реализовал одноэлементный класс, который хранит данные, пока приложение работает (кстати, есть ли другой способ сделать это?).

RetrofitService

private val retrofitService = Retrofit.Builder()
.addConverterFactory(
    GsonConverterFactory
        .create(
            GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .setLenient().setDateFormat("yyyy-MM-dd")
                .create()
        )
)
.addConverterFactory(RetrofitConverter.create())
.baseUrl(BASE_URL)
.build()
`object ApiObject {
val retrofitService: ApiInterface by lazy { 
retrofitBuilder.create(ApiInterface::class.java) }
}

ApiInterface

interface ApiInterface {

@GET("workstation/{date}")
suspend fun getWorkstations(
    @Path("date") date: Date
): List<Workstation>

@GET("reservation/{date}")
suspend fun getReservations(
    @Path("date") date: Date
): List<Reservation>

@GET("user")
suspend fun getUsers(): List<User>

@GET("user/login")
suspend fun validateLoginCredentials(
    @Query("username") username: String,
    @Query("password") password: String
): Response<User>

ApiResponse

sealed class ApiResponse<T> {
    companion object {
        fun <T> create(response: Response<T>): ApiResponse<T> {
            return if(response.isSuccessful) {
                val body = response.body()
                // Empty body
                if (body == null || response.code() == 204) {
                    ApiSuccessEmptyResponse()
                } else {
                    ApiSuccessResponse(body)
                }
            } else {
                val msg = response.errorBody()?.string()
                val errorMessage = if(msg.isNullOrEmpty()) {
                    response.message()
                } else {
                    msg.let {
                        return@let JSONObject(it).getString("message")
                    }
                }
                ApiErrorResponse(errorMessage ?: "Unknown error")
            }
        }
    }
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()

Репозиторий

class Repository {

companion object {

    private var instance: Repository? = null

    fun getInstance(): Repository {
        if (instance == null)
            instance = Repository()
        return instance!!
    }
}

private var singletonClass = SingletonClass.getInstance()

suspend fun validateLoginCredentials(username: String, password: String) {
    withContext(Dispatchers.IO) {
        val result: Response<User>?
        try {
            result = ApiObject.retrofitService.validateLoginCredentials(username, password)
            when (val response = ApiResponse.create(result)) {
                is ApiSuccessResponse -> {
                    singletonClass.loggedUser = response.data
                }
                is ApiSuccessEmptyResponse -> throw Exception("Something went wrong")
                is ApiErrorResponse -> throw Exception(response.errorMessage)
            }
        } catch (error: Exception) {
            throw error
        }
    }
}

suspend fun getWorkstationsListFromService(date: Date) {
    withContext(Dispatchers.IO) {
        val workstationsListResult: List<Workstation>
        try {
            workstationsListResult = ApiObject.retrofitService.getWorkstations(date)
            singletonClass.rWorkstationsList.postValue(workstationsListResult)
        } catch (error: Exception) {
            throw error
        }
    }
}

suspend fun getReservationsListFromService(date: Date) {
    withContext(Dispatchers.IO) {
        val reservationsListResult: List<Reservation>
        try {
            reservationsListResult = ApiObject.retrofitService.getReservations(date)
            singletonClass.rReservationsList.postValue(reservationsListResult)
        } catch (error: Exception) {
            throw error
        }
    }
}

suspend fun getUsersListFromService() {
    withContext(Dispatchers.IO) {
        val usersListResult: List<User>
        try {
            usersListResult = ApiObject.retrofitService.getUsers()
            singletonClass.rUsersList.postValue(usersListResult.let { usersList ->
                usersList.filterNot { user -> user.username == "admin" }
                    .sortedWith(Comparator { x, y -> x.surname.compareTo(y.surname) })
            })
        } catch (error: Exception) {
            throw error
        }
    }
}

SingletonClass

const val FAILED = 0
const val COMPLETED = 1
const val RUNNING = 2

class SingletonClass private constructor() {
companion object {
    private var instance: SingletonClass? = null

    fun getInstance(): SingletonClass {
        if (instance == null)
            instance = SingletonClass()
        return instance!!
    }
}

//User
var loggedUser: User? = null

//Workstations List
val rWorkstationsList = MutableLiveData<List<Workstation>>()

//Reservations List
val rReservationsList = MutableLiveData<List<Reservation>>()

//Users List
val rUsersList = MutableLiveData<List<User>>()
}

ViewModel

class ViewModel : ViewModel() {

private val singletonClass = SingletonClass.getInstance()

private val repository = Repository.getInstance()

//MutableLiveData
//Login
private val _loadingStatus = MutableLiveData<Boolean>()

val loadingStatus: LiveData<Boolean>
    get() = _loadingStatus

private val _successfulAuthenticationStatus = MutableLiveData<Boolean>()

val successfulAuthenticationStatus: LiveData<Boolean>
    get() = _successfulAuthenticationStatus

//Data fetch
private val _listsLoadingStatus = MutableLiveData<Int>()

val listsLoadingStatus: LiveData<Int>
    get() = _listsLoadingStatus

private val _errorMessage = MutableLiveData<String>()

val errorMessage: LiveData<String>
    get() = _errorMessage

fun onLoginClicked(username: String, password: String) {
    launchLoginAuthentication {
        repository.validateLoginCredentials(username, password)
    }
}

private fun launchLoginAuthentication(block: suspend () -> Unit): Job {
    return viewModelScope.launch {
        try {
            _loadingStatus.value = true
            block()
        } catch (error: Exception) {
            _errorMessage.postValue(error.message)
        } finally {
            _loadingStatus.value = false
            if (singletonClass.loggedUser != null)
                _successfulAuthenticationStatus.value = true
        }
    }
}

fun onLoginPerformed() {
    val date = Calendar.getInstance().time
    launchListsFetch {
//how to start these all at the same time? Then wait until their competion
//and call the two methods below?
        repository.getReservationsListFromService(date)
        repository.getWorkstationsListFromService(date)
        repository.getUsersListFromService()
    }

}

private fun launchListsFetch(block: suspend () -> Unit): Job {
    return viewModelScope.async {
        try {
            _listsLoadingStatus.value = RUNNING
            block()
        } catch (error: Exception) {
            _listsLoadingStatus.value = FAILED
            _errorMessage.postValue(error.message)
        } finally {
//I'd like to perform these operations at the same time
           prepareWorkstationsList()
           prepareReservationsList()
//and, when both completed, set this value 
           _listsLoadingStatus.value = COMPLETED
        }
    }
}

    fun onToastShown() {
        _errorMessage.value = null
    }
}

LoginActivity

class LoginActivity : AppCompatActivity() {

private val viewModel: LoginViewModel
    get() = ViewModelProviders.of(this).get(LoginViewModel::class.java)

private val loadingFragment = LoadingDialogFragment()

var username = ""
var password = ""

private lateinit var loginButton: Button
lateinit var context: Context

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)

    loginButton = findViewById(R.id.login_button)

    loginButton.setOnClickListener {
        username = login_username.text.toString().trim()
        password = login_password.text.toString().trim()
        viewModel.onLoginClicked(username, password.toMD5())
    }

    viewModel.loadingStatus.observe(this, Observer { value ->
        value?.let { show ->
            progress_bar_login.visibility = if (show) View.VISIBLE else View.GONE
        }
    })

    viewModel.successfulAuthenticationStatus.observe(this, Observer { successfullyLogged ->
        successfullyLogged?.let {
            loadingFragment.setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomLoadingDialogFragment)
            if (successfullyLogged) {
                loadingFragment.show(supportFragmentManager, "loadingFragment")
                viewModel.onLoginPerformed()
            } else {
                login_password.text.clear()
                login_password.isFocused
                password = ""
            }
        }
    })

    viewModel.listsLoadingStatus.observe(this, Observer { loadingResult ->
        loadingResult?.let {
            when (loadingResult) {
                COMPLETED -> {
                    val intent = Intent(this, MainActivity::class.java)
                    startActivity(intent)
                    setResult(Activity.RESULT_OK)
                    finish()
                }
                FAILED -> {
                    loadingFragment.changeText("Error")
                    loadingFragment.showProgressBar(false)
                    loadingFragment.showRetryButton(true)
                }
            }
        }
    })

    viewModel.errorMessage.observe(this, Observer { value ->
        value?.let { message ->
            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
            viewModel.onToastShown()
        }
    })
}

По сути, я пытаюсь отправить имя пользователя и пароль, показать индикатор выполнения во время ожидания результата (в случае успешного возврата зарегистрированного объекта пользователя, в противном случае - тост).с сообщением об ошибке), скрыть индикатор выполнения и показать фрагмент загрузки.При отображении фрагмента загрузки запустите 3 асинхронных сетевых вызова и дождитесь их завершения;когда третий вызов завершен, запустите методы для обработки данных и, когда оба сделаете, начните следующее действие.

Кажется, что все работает просто отлично, но при отладке я заметил поток (в основном, сетевые вызовы).start / wait / onCompletion) совсем не то, что я описал выше.Думаю, что-то нужно исправить в ViewModel, но я не могу понять, что

...