recyclerViwe در کاتلین


recyclerViwe در کاتلین همراه با اتصال به دیتابیس نامحدود

در این آموزش به پیاده سازی recyclerviwe در کاتلین برای نمایش دیتابیسی بزرگ یا نامحدود می پردازیم. ریسایکلرویو عملی بازیافتی برای view ها به شمار می رود. از این طریق می توان مطمئن بود که منابع هدر نمی روند و دوباره به شکل بهینه ای استفاده خواهند شد. recyclerView موضوع آموزش های پیشین بلاگ بود. پیاده سازی و نحوه کار بهینه با آن در مقاله های مختلفی بررسی شد. تمام آموزش های پیشین در این باره به زبان جاوا بودند. امروز با پیاده سازی recyclerViwe در کاتلین با شما خواهیم بود.

ایجاد پروژه در حالت ابتدایی

قبل از پیاده سازی recyclerViwe در کاتلین، غالب کلی پروژه را می سازیم. این حالت کلی برنامه پیش از اتصال به api برای دریافت داده هاست. کلاس هایی که باید بسازیم :

  • کلاسی برای دریافت تصاویر
  • کلاسی برای نمایش تصاویر
  • کلاسی برای دریافت اطلاعات تصویر به صورت json
  • کلاس MainActivity

تعریف dependency های پروژه

تمام آنچه برای این پروژه لازم است در dependency موجود باشد به صورت زیر می باشد :

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    implementation "com.android.support:appcompat-v7:$support_version"
    implementation "com.android.support:recyclerview-v7:$support_version"
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.squareup.picasso:picasso:2.5.2'
    implementation 'com.squareup.okhttp3:okhttp:3.12.0'
    implementation "com.android.support:support-v4:$support_version"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

هم چنین targetSdkVersion و compileSdkVersion روی 28 ست شده است.

ساخت کلاس برای دریافت تصاویر

mport android.app.Activity
import android.content.Context
import android.net.Uri.Builder
import okhttp3.*
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class ImageRequester(listeningActivity: Activity) {

  interface ImageRequesterResponse {
    fun receivedNewPhoto(newPhoto: Photo)
  }

  private val calendar: Calendar = Calendar.getInstance()
  private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
  private val responseListener: ImageRequesterResponse
  private val context: Context
  private val client: OkHttpClient
  var isLoadingData: Boolean = false
    private set

  init {
    responseListener = listeningActivity as ImageRequesterResponse
    context = listeningActivity.applicationContext
    client = OkHttpClient()
  }

  fun getPhoto() {

    val date = dateFormat.format(calendar.time)

ساخت کلاسی برای دریافت اطلاعات تصویر

import org.json.JSONException
import org.json.JSONObject
import java.io.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*

class Photo(photoJSON: JSONObject) : Serializable {

  private lateinit var photoDate: String
  lateinit var humanDate: String
    private set
  lateinit var explanation: String
    private set
  lateinit var url: String
    private set

  init {
    try {
      photoDate = photoJSON.getString(PHOTO_DATE)
      humanDate = convertDateToHumanDate()
      explanation = photoJSON.getString(PHOTO_EXPLANATION)
      url = photoJSON.getString(PHOTO_URL)
    } catch (e: JSONException) {
      e.printStackTrace()
    }

  }

  private fun convertDateToHumanDate(): String {

    val dateFormat = SimpleDateFormat("yyyy-MM-dd")
    val humanDateFormat = SimpleDateFormat("dd MMMM yyyy")
    try {
      val parsedDateFormat = dateFormat.parse(photoDate)
      val cal = Calendar.getInstance()
      cal.time = parsedDateFormat
      return humanDateFormat.format(cal.time)
    } catch (e: ParseException) {
      e.printStackTrace()
      return ""
    }

  }

  companion object {
    private val PHOTO_DATE = "date"
    private val PHOTO_EXPLANATION = "explanation"
    private val PHOTO_URL = "url"
  }
}

ساخت کلاس اکتیویتی برای نمایش تصاویر

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_photo.*

class PhotoActivity : AppCompatActivity() {

  private var selectedPhoto: Photo? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_photo)

    selectedPhoto = intent.getSerializableExtra(PHOTO_KEY) as Photo
    Picasso.with(this).load(selectedPhoto?.url).into(photoImageView)

    photoDescription?.text = selectedPhoto?.explanation
  }

  companion object {
    private val PHOTO_KEY = "PHOTO"
  }
}

کلاس MainActivity در آغاز پروژه

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import java.io.IOException
import java.util.*

class MainActivity : AppCompatActivity(), ImageRequester.ImageRequesterResponse {

  private var photosList: ArrayList<Photo> = ArrayList()
  private lateinit var imageRequester: ImageRequester

  override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)
    return true
  }

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

    imageRequester = ImageRequester(this)
  }

  override fun onStart() {
    super.onStart()
  }

  private fun requestPhoto() {
    try {
      imageRequester.getPhoto()
    } catch (e: IOException) {
      e.printStackTrace()
    }

  }

  override fun receivedNewPhoto(newPhoto: Photo) {
    runOnUiThread {
      photosList.add(newPhoto)
    }
  }
}

پس در آغاز پروژه باید شبیه تصویر زیر باشد. حالا آماده اتصال به دیتابیس می باشد.

در این پروژه به سراغ یکی از سرویس های محبوب ناسا می رویم. برای استفاده از این api به api key نیاز داریم. با رفتن به این صفحه و ثبت ایمیل و نام خود می توانید api key دریافت کنید. پس از دریافت این کد وارد فایل Strings.xml شوید و در تگ resources به شکل زیر آن را تعریف کنید :

<resources>
   <string name="app_name">Galaction</string>
   <string name="api_key">INSERT YOUR API KEY HERE</string>

   <string name="title_activity_fullscreen">FullscreenActivity</string>
   <string name="change_layout_manager">Change Layout</string>
<resources/>

ریسایکلرویو از نمای نزدیک!

اندروید پیش از این برای نمایش انواع لیست ها از GridView و ListView استفاده می کرد. recyclerView ترکیبی از این دو می باشد. گرچه فارغ از این موضوع recyclerView امکاناتی دارد که با استفاده از آن کد برنامه تبدیل به کدی منعطف، قابل ارجاع و استفاده مجدد در برنامه های دیگر می شود.

سوالی که مطرح می شود این است که چه لزومی به استفاده از recyclerView با وجود ListView ست؟

فرض کنید با استفاده از ListView آیتم های پیچیده ای را برای نمایش در لیستی ساخته اید. در متد ()getView هر آیتم inflate یا نقاشی می شود. در واقع در پروسه بازیافتی این روند ListView و GridView نیمی از کار را به عهده می گیرند و نمی توان با وجود آنها حقیقتا استفاده بهینه ای از حافظه نمود. آنها آیتم ها را بازیافت می کنند اما ارجاع خود به آنها را حفظ نمی کنند پس برای هر آیتم نیاز به استفاده از findViewById ست که عملی سنگین و هزینه بر برای سیستم محسوب می شود.

راه حلی که اندروید برای این مشکل در نظر گرفته که عملیات scrolling را بسیار روان می کند، استفاده از الگوی ViewHolder است. با این الگو کلاس در حافظه ارجاع هایی را برای هر view در نظر می گیرد. شما یک بار برای هر view ارجاعی در حافظه می سازید و بارها استفاده می کنید. به این ترتیب پرفورمنس برنامه تا حد زیادی با جلوگیری از استفاده مکرر findViewById بهبود می یابد.

اما به هر حال حتی با بکارگیری این الگو هم چنان LIstView و GridView عملکرد آهسته ای دارند.

RecyclerView با وجود adapter که منبع داده های آن است و ViewHolder که ارجاع و رفرنس ها را در حافظه نگه می دارد عملکردی بهینه و پرفورمنسی روان را می سازد.

از طرفی RecyclerView با امکان جذاب LayoutManager به راحتی نحوه قرارگیری آیتم ها را به سه شکل ممکن می سازند :

  • LinearLayoutManager نمایشی مثل یک ListView استاندارد می سازد.
  • GridLayoutManager نمایشی مثل حالت GridView را می سازد.
  • StaggeredGridLayoutManager 

و اما پیاده سازی RecyclerView !

برای ساخت ریسایکلرویو چهار قدم باید عملی شوند :

  1. تعریف recyclerView در layout مربوط به اکتیویتی و ایجاد رفرنسی از آن در اکتیویتی برنامه
  2. ساخت یک layout اختصاصی برای آیتم های ریسایکلرویو
  3. ساخت کلاس view holder برای viewی مربوط به هر آیتم، اتصال به منبع داده مورد نظر recyclerView و کنترل منطق کار ریسایکلرویو با ساخت adapter
  4. اتصال adapter به recyclerView

تعریف ریسایکلرویو در layout اکتیویتی برنامه

<android.support.v7.widget.RecyclerView
  android:id="@+id/recyclerView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:scrollbars="vertical"/>

نکته : می توانید به چای import دستی کتابخانه های لازم برای پروژه در اندروید استودیو امکان  auto-add imports را فعال نمایید تا به شکل خودکار هر کتابخانه ای که در پروژه از آن استفاده می کنید import شود.

در کلاس MainActivity ریسایکلرویو را تعریف می کنیم :

private lateinit var linearLayoutManager: LinearLayoutManager

در ()onCreate نیز بعد از خط setContetnView کد زیر اضافه می شود :

linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager

شاید تعجب کنید که چطور قبل از استفاده از findViewById می توانید ارجاعی به ریسایکلرویو بسازید. اما با وجود پلاگین های کاتلین به شکل مستقیمی می توان از کلاس اکتیویتی به خصوصیات xml آن دست یافت. اگر با جاوا ریسایکلرویو را پیاده سازی کرده باشید ، می بینید که به کمک این پلاگین از مقدار زیادی کدنویسی جلوگیری شده است.

ساخت layout برای آیتم های recyclerView

فایل xml ی با نام recyclerview_item_row می سازیم. برای این view از ConstraintLayout استفاده می کنیم و به شکل زیر آن را طراحی می نماییم :

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:padding="8dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

در این لایه موارد لازم برای آیتم را مثل زیر تعریف می کنیم :

<ImageView
     android:id="@+id/itemImage"
     android:layout_width="0dp"
     android:layout_height="wrap_content"
     android:layout_marginTop="8dp"
     android:adjustViewBounds="true"
     app:layout_constraintBottom_toTopOf="@+id/itemDate"
     app:layout_constraintEnd_toEndOf="parent"
     app:layout_constraintHorizontal_bias="0.5"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toTopOf="parent"
     app:layout_constraintVertical_bias="0.74" />

 <TextView
     android:id="@+id/itemDate"
     android:layout_width="0dp"
     android:layout_height="wrap_content"
     android:layout_gravity="top|start"
     android:layout_marginTop="8dp"
     android:layout_weight="1"
     app:layout_constraintBottom_toTopOf="@+id/itemDescription"
     app:layout_constraintEnd_toEndOf="parent"
     app:layout_constraintHorizontal_bias="0.5"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toBottomOf="@+id/itemImage"
     tools:text="Some date" />

 <TextView
     android:id="@+id/itemDescription"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center|start"
     android:layout_weight="1"
     android:ellipsize="end"
     android:maxLines="5"
     app:layout_constraintBottom_toBottomOf="parent"
     app:layout_constraintEnd_toEndOf="parent"
     app:layout_constraintHorizontal_bias="0.5"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toBottomOf="@+id/itemDate" />

ساخت آداپتر برای ریسایکلرویو

کلاس جدیدی با نام RecyclerAdapter می سازیم. این کلاس از RecyclerView.Adapter ارث بری می کند.

class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapter.PhotoHolder>()  {
}

با وجود ایرادی که در این حالت از کلاس گرفته می شود، متوجه می شویم که باید متدهای ضروری کلاس پدر را پیاده سازیم.

هر سه متد را انتخاب و پیاده می سازیم. getItemCount و onBindViewHolder و onCreateViewHolder. برای آشنایی با عملکرد این متدها می توانید به این مقاله رجوع کنید.

recyclerView به شکل اختصاصی امکانی برای کلیک روی آیتم ها تعریف نکرده است. به خاطر اینکه تمرکز recyclerView بر روی جایگاه و قرار گیری صحیح آیتم هاست. البته با وجود این مدیریت و ارجاع روی آیتم ها به بهترین شکل می توانیم ClickListener ی برای آن تعبیه سازیم.

به عنوان فیلدی از کلاس RecyclerViewAdapter متغیر photos را تعریف می کنیم. به گونه ای که مقدار دهی اولیه آن در constructor کلاس باشد.

private val photos: ArrayList<Photo>

و ست شدن مقدار آن :

class RecyclerAdapter(private val photos: ArrayList<Photo>) RecyclerView.Adapter<RecyclerAdapter.PhotoHolder>() {

حالا کلاس recyclerView می داند که از طریق چه فیلدی باید داده ها را دریافت کند.

از طرفی getItemCount حالا به راحتی از تعداد داده ها آگاه می شود :

override fun getItemCount() = photos.size

ساخت کلاس ViewHolder

ارجاع داده ها در کلاسی مدیریت می شود. کلاس تو در تویی که در کلاس آداپتر ساخته می شود. چرا که رفتار کلاس ViewHolder در گرو کلاس آداپتر است.

//1
class PhotoHolder(v: View) : RecyclerView.ViewHolder(v), View.OnClickListener {
  //2
  private var view: View = v
  private var photo: Photo? = null

  //3
  init {
    v.setOnClickListener(this)
  }

  //4
  override fun onClick(v: View) {
    Log.d("RecyclerView", "CLICK!")
  }

  companion object {
    //5
    private val PHOTO_KEY = "PHOTO"
  }
}

حالا به سراغ سه متد اصلی آداپتر می رویم و آن ها را کامل می کنیم :

override fun onBindViewHolder(holder: RecyclerAdapter.PhotoHolder, position: Int) {
    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.PhotoHolder {
    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
 }

برای کشیده شدن هر آیتم روی صفحه نمایش هر بار متد onCreateViewHolder صدا زده می شود. در این متد قطعه کد زیر را وارد می کنیم :

fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
    return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}

به جای خط TODO(“not implemented”) نیز قطعه کد زیر را جایگزین می کنیم:

val inflatedView = parent.inflate(R.layout.recyclerview_item_row, false)
return PhotoHolder(inflatedView)

در شروع کار اکتیویتی جدیدی برای دریافت اطلاعات آیتم مورد نظر در بخش onClick چنین کدی را می نویسیم :

val context = itemView.context
val showPhotoIntent = Intent(context, PhotoActivity::class.java)
showPhotoIntent.putExtra(PHOTO_KEY, photo)
context.startActivity(showPhotoIntent)

به این شکل محتوای آیتم انتخابی با id منحصر بفرد آیتم ردیابی شده و بصورت intent ی در اختیار اکتیویتی جدید برای نمایش یک آیتم قرار داده می شود.

متدی که تصویر آیتم را نمایش می دهد به شکل زیر ساخته می شود :

fun bindPhoto(photo: Photo) {
  this.photo = photo
  Picasso.with(view.context).load(photo.url).into(view.itemImage)
  view.itemDate.text = photo.humanDate
  view.itemDescription.text = photo.explanation
}

ملاحظه می کنید که برای دریافت و نمایش تصویر از URL آن از کتابخانه Picasso استفاده شده است.

آخرین قسمتی که هنوز کامل نشده متد onBindViewHolder است. این بخش وجود آیتم جدید و داده مربوط به آن را کنترل می کند.

val itemPhoto = photos[position]
holder.bindPhoto(itemPhoto)

اتصال adapter و recyclerView

در کلاس MainActivity فیلد جدیدی به شکل زیر تعریف می کنیم:

private lateinit var adapter: RecyclerAdapter

و سپس برای انتخاب نحوه قرارگیری آیتم ها :

adapter = RecyclerAdapter(photosList)
recyclerView.adapter = adapter

بدیهی ست تا اینجای کار با یک صفحه نمایش خالی رو به رو هستیم چراکه هنوز داده ای به ریساکلرویو منتقل نشده. در متد onStart اکتیویتی :

if (photosList.size == 0) {
  requestPhoto()
}

برای دریافت تصویر در متد receivedNewPhoto این خط را اضافه می کنیم :

adapter.notifyItemInserted(photosList.size-1)

متد receivedNewPhoto به شکل زیر کار خود را عملی می سازد :

override fun receivedNewPhoto(newPhoto: Photo) {
  runOnUiThread {
    photosList.add(newPhoto)
    adapter.notifyItemInserted(photosList.size-1)
  }
}

به این شکل آداپتر از افزوده شدن آیتم جدیدی آگاه می گردد.

اگر در این مرحله از برنامه تست بگیریم به شکل زیر اجرا می گردد :

و با زدن بر روی آیتم در اکتیویتی دیگری نمایش داده می شود :

 

 

به این پست امتیاز دهید

روی ستاره های کلیک کنید و امتیاز بدید

میانگین امتیاز / 5. تعداد:

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *




Enter Captcha Here : *

Reload Image