我已经将RAVI TAMADA的RxJava教程从Java转换为Kotlin Android RxJava Networking with Retrofit, Gson – Notes App。仅在第一个网络调用上测试应用程序时,我收到HTTP 500内部错误(com.jakewharton.retrofit2.adapter.rxjava2.HttpException:HTTP 500内部服务器错误)。这是第一次网络通话。 registerUsers 就转换而言,我已经完成了本书的所有工作。
我已经包括了以下课程的代码库
MainActivity
class MainActivity : AppCompatActivity()
{
lateinit var apiService: ApiService
var disposable = CompositeDisposable()
lateinit var mAdapter: NotesAdapter
var noteList = ArrayList<Note>()
companion object
{
val TAG = MainActivity::class.java.simpleName;
}
@BindView(R.id.coordinator_layout) var coordinatorLayout: CoordinatorLayout? = null
@BindView(R.id.recycler_view) var recyclerView: RecyclerView? = null
@BindView(R.id.txt_empty_notes_view) var noNotesView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(getString(R.string.activity_title_home))
setSupportActionBar(toolbar)
fab.setOnClickListener { view ->
showNoteDialog(false, null, -1);
}
// white background notification bar
whiteNotificationBar(fab);
apiService = ApiClient.getClient(getApplicationContext())?.create(ApiService::class.java)!!
mAdapter = NotesAdapter(this, noteList)
var mLayoutManager = LinearLayoutManager(getApplicationContext());
recyclerView?.setLayoutManager(mLayoutManager);
recyclerView?.setItemAnimator(DefaultItemAnimator());
recyclerView?.addItemDecoration(MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL, 16));
recyclerView?.setAdapter(mAdapter);
/**
* On long press on RecyclerView item, open alert dialog
* with options to choose
* Edit and Delete
* */
recyclerView?.addOnItemTouchListener(RecyclerTouchListener(this, recyclerView!!, object : RecyclerTouchListener.ClickListener
{
override fun onClick(view: View, position: Int)
{
}
override fun onLongClick(view: View, position: Int)
{
showActionsDialog(position);
}
}))
/**
* Check for stored Api Key in shared preferences
* If not present, make api call to register the user
* This will be executed when app is installed for the first time
* or data is cleared from settings
* */
var test = PrefUtils.getApiKey(this)
if (TextUtils.isEmpty(PrefUtils?.getApiKey(this)))
{
registerUser();
} else
{
// user is already registered, fetch all notes
fetchAllNotes();
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean
{
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean
{
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId)
{
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
/**
* Registering new user
* sending unique id as device identification
* https://developer.android.com/training/articles/user-data-ids.html
*/
private fun registerUser()
{
// unique id to identify the device
val uniqueId = UUID.randomUUID().toString()
disposable
.add(apiService.register(uniqueId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(
object : DisposableSingleObserver<User>()
{
override fun onSuccess(user: User)
{
// Storing user API Key in preferences
user.apiKey?.let { PrefUtils.storeApiKey(applicationContext, it) }
Toast.makeText(applicationContext,
"Device is registered successfully! ApiKey: " + PrefUtils.getApiKey(applicationContext),
Toast.LENGTH_LONG).show()
}
override fun onError(e: Throwable)
{
Log.e(TAG, "onError: " + e.message)
showError(e)
}
}))
}
/**
* Fetching all notes from api
* The received items will be in random order
* map() operator is used to sort the items in descending order by Id
*/
fun fetchAllNotes()
{
disposable.add(apiService.fetchAllNotes().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).map(
object : io.reactivex.functions.Function<List<Note>, List<Note>>
{
override fun apply(notes: List<Note>): List<Note>
{
Collections.sort(notes, object : Comparator<Note>
{
override fun compare(n1: Note?, n2: Note?): Int
{
return n2!!.id - n1!!.id;
}
})
return notes
}
}).subscribeWith(object : DisposableSingleObserver<List<Note>>()
{
override fun onSuccess(notes: List<Note>)
{
noteList.clear();
noteList.addAll(notes);
mAdapter.notifyDataSetChanged();
toggleEmptyNotes();
}
override fun onError(e: Throwable)
{
Log.e(TAG, "onError: " + e.message);
showError(e);
}
}))
}
/**
* Creating new note
*/
private fun createNote(note: String)
{
disposable.add(apiService.createNote(note)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(
object : DisposableSingleObserver<Note>()
{
override fun onSuccess(note: Note)
{
if (!TextUtils.isEmpty(note.error))
{
Toast.makeText(applicationContext, note.error, Toast.LENGTH_LONG).show()
return
}
Log.d(TAG, "new note created: " + note.id + ", " + note.note + ", " + note.timestamp)
// Add new item and notify adapter
noteList.add(0, note)
mAdapter.notifyItemInserted(0)
toggleEmptyNotes()
}
override fun onError(e: Throwable)
{
Log.e(TAG, "onError: " + e.message)
showError(e)
}
}))
}
/**
* Updating a note
*/
private fun updateNote(noteId: Int, note: String, position: Int)
{
disposable
.add(apiService.updateNote(noteId,
note)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object :
DisposableCompletableObserver()
{
override fun onComplete()
{
Log.d(TAG, "Note updated!")
val n = noteList.get(position)
n.note = (note)
// Update item and notify adapter
noteList.set(position, n)
mAdapter.notifyItemChanged(position)
}
override fun onError(e: Throwable)
{
Log.e(TAG, "onError: " + e.message)
showError(e)
}
}))
}
/**
* Deleting a note
*/
private fun deleteNote(noteId: Int, position: Int)
{
Log.e(TAG, "deleteNote: $noteId, $position")
disposable
.add(apiService.deleteNote(noteId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(
object : DisposableCompletableObserver()
{
override fun onComplete()
{
Log.d(TAG, "Note deleted! $noteId")
// Remove and notify adapter about item deletion
noteList.removeAt(position)
mAdapter.notifyItemRemoved(position)
Toast.makeText(this@MainActivity, "Note deleted!", Toast.LENGTH_SHORT).show()
toggleEmptyNotes()
}
override fun onError(e: Throwable)
{
Log.e(TAG, "onError: " + e.message)
showError(e)
}
}))
}
/**
* Shows alert dialog with EditText options to enter / edit
* a note.
* when shouldUpdate=true, it automatically displays old note and changes the
* button text to UPDATE
*/
private fun showNoteDialog(shouldUpdate: Boolean, note: Note?, position: Int)
{
val layoutInflaterAndroid = LayoutInflater.from(applicationContext)
val view = layoutInflaterAndroid.inflate(R.layout.note_dialog, null)
val alertDialogBuilderUserInput = AlertDialog.Builder(this@MainActivity)
alertDialogBuilderUserInput.setView(view)
val inputNote = view.findViewById<EditText>(R.id.note)
val dialogTitle = view.findViewById<TextView>(R.id.dialog_title)
dialogTitle.setText(if (!shouldUpdate) getString(R.string.lbl_new_note_title) else getString(R.string.lbl_edit_note_title))
if (shouldUpdate && note != null)
{
inputNote.setText(note.note)
}
alertDialogBuilderUserInput.setCancelable(false).setPositiveButton(if (shouldUpdate) "update" else "save",
DialogInterface.OnClickListener { dialogBox, id -> })
.setNegativeButton("cancel", DialogInterface.OnClickListener { dialogBox, id -> dialogBox.cancel() })
val alertDialog = alertDialogBuilderUserInput.create()
alertDialog.show()
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(View.OnClickListener {
// Show toast message when no text is entered
if (TextUtils.isEmpty(inputNote.text.toString()))
{
Toast.makeText(this@MainActivity, "Enter note!", Toast.LENGTH_SHORT).show()
return@OnClickListener
} else
{
alertDialog.dismiss()
}
// check if user updating note
if (shouldUpdate && note != null)
{
// update note by it's id
updateNote(note.id, inputNote.text.toString(), position)
} else
{
// create new note
createNote(inputNote.text.toString())
}
})
}
/**
* Opens dialog with Edit - Delete options
* Edit - 0
* Delete - 0
*/
private fun showActionsDialog(position: Int)
{
val colors = arrayOf<CharSequence>("Edit", "Delete")
val builder = AlertDialog.Builder(this)
builder.setTitle("Choose option")
builder.setItems(colors) { dialog, which ->
if (which == 0)
{
showNoteDialog(true, noteList.get(position), position)
} else
{
deleteNote(noteList.get(position).id, position)
}
}
builder.show()
}
private fun toggleEmptyNotes()
{
if (noteList.size > 0)
{
noNotesView?.setVisibility(View.GONE)
} else
{
noNotesView?.setVisibility(View.VISIBLE)
}
}
/**
* Showing a Snackbar with error message
* The error body will be in json format
* {"error": "Error message!"}
*/
fun showError(e: Throwable)
{
var message = ""
try
{
if (e is IOException)
{
message = "No internet connection!"
}
else (e is HttpException)
run {
var error = e as HttpException
var errorBody = error.response().errorBody().toString()
var jObj = JSONObject(errorBody)
message = jObj.getString("error")
}
}
catch (e1: IOException)
{
e1.printStackTrace()
}
catch (e1: JSONException)
{
e1.printStackTrace()
}
catch (e1: Exception)
{
e1.printStackTrace()
}
if (TextUtils.isEmpty(message))
{
message = "Unknown error occurred! Check LogCat."
}
val snackbar = coordinatorLayout?.let { Snackbar.make(it, message, Snackbar.LENGTH_LONG) }
val sbView = snackbar?.getView()
val textView = sbView?.findViewById<TextView>(android.support.design.R.id.snackbar_text)
textView?.setTextColor(Color.YELLOW)
snackbar?.show()
}
fun whiteNotificationBar(view: View)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
var flags = view.getSystemUiVisibility()
flags = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
view.setSystemUiVisibility(flags)
getWindow().setStatusBarColor(Color.WHITE)
}
}
override fun onDestroy()
{
super.onDestroy()
disposable.dispose()
}
}
ApiService界面
interface ApiService
{
// Register new user
@FormUrlEncoded
@POST("notes/user/register")
fun register(@Field("device_id") deviceId: String): Single<User>
// Single<User> register(@Field("device_id") String deviceId)
// Create note
@FormUrlEncoded
@POST("notes/new")
fun createNote(@Field("note") note: String): Single<Note>
// Fetch all notes
@GET("notes/all") fun fetchAllNotes(): Single<List<Note>>
// Update single note
@FormUrlEncoded
@PUT("notes/{id}")
fun updateNote(@Path("id") noteId: Int, @Field("note") note: String): Completable
// Delete note
@DELETE("notes/{id}")
fun deleteNote(@Path("id") noteId: Int): Completable
}
ApiClient类
class ApiClient
{
companion object
{
var retrofit: Retrofit? = null
var REQUEST_TIMEOUT = 60
var okHttpClient: OkHttpClient? = null
fun getClient(context: Context): Retrofit?
{
if (okHttpClient == null)
initOkHttp(context)
if (retrofit == null)
{
retrofit = Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create()).build()
}
return retrofit
}
fun initOkHttp(context: Context)
{
val httpClient = OkHttpClient().newBuilder().connectTimeout(REQUEST_TIMEOUT.toLong(), TimeUnit.SECONDS)
.readTimeout(REQUEST_TIMEOUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(REQUEST_TIMEOUT.toLong(), TimeUnit.SECONDS)
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient.addInterceptor(interceptor)
httpClient.addInterceptor(object : Interceptor
{
override fun intercept(chain: Interceptor.Chain): Response
{
var original = chain.request()
var requestBuilder = original.newBuilder()
.addHeader("Accept", "application/json").addHeader("Content-Type", "application/json");
// Adding Authorization token (API Key)
// Requests will be denied without API key
if (!TextUtils.isEmpty(PrefUtils.getApiKey(context)))
{
requestBuilder.addHeader("Authorization", PrefUtils.getApiKey(context));
}
var request = requestBuilder.build();
return chain.proceed(request)
}
})
okHttpClient = httpClient.build()
}
}
}
PrefUtils类
class PrefUtils
{
/**
* Storing API Key in shared preferences to
* add it in header part of every retrofit request
*/
companion object
{
fun getSharedPreferences(context: Context): SharedPreferences
{
return context.getSharedPreferences("APP_PREF", Context.MODE_PRIVATE)
}
fun storeApiKey(context: Context, apiKey: String)
{
val editor = getSharedPreferences(context).edit()
editor.putString("API_KEY", apiKey)
editor.commit()
}
fun getApiKey(context: Context): String?
{
return getSharedPreferences(context).getString("API_KEY", null)
}
}
}