PDA

View Full Version : سوال: مشکل در نمایش سلسه مراتب درختی با زبان کاتلین



amirreza_dq
یک شنبه 02 شهریور 1399, 01:42 صبح
درود و وقت بخیر...

می‌خواستم ازتون خواهش کنم که میشه لطف کنین و در مورد پیاده سازی این موردی که الان توضیح خواهم داد کمی راهنماییم کنین...

خب...

قراره یه سلسه مراتب درختی رو در قالب یه اپ ساده Todo و با زبان کاتلین و با استفاده از دیزاین پترن‌ها، معماری MVVM، کتابخونه‌های JetPack و اگه بتونم هم از Hilt برای پیاده سازیش استفاده کنم...


و اساس کار به این صورت هستش که که هر گره (todo) می‌تونه بی‌نهایت زیر مجموعه «از جنس خودش» داشته باشه و هر زیر مجموعه هم بینهایت زیر مجموعه و الی آخر...
همچنین اگه رو گره (todo) پدر کلیک شد، تمام زیر مجموعه هاش هم تیک بخورن
و بر عکسش هم به این صورت که اگه، تیک گره(todo) فرزند رو برداریم، تیک تمام گره های پدر هم برداشته بشه...

این از طرح کلی برنامه...
و اما مشکلی که بهش برخوردم...

تا اینجا پیش رفتم که یه لیستی دارم و می‌تونم بهش todo اضافه کنم و اون todo ها رو در قالب ریسایکلر ویو نمایش بدم...

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

152070

مدل todoی که تعریف کردم شامل فیلدهای زیر هستش:

var name: String,
var parent_id: Long ,
var id: Long


به این صورت کار کردم که اومدم و id آیتمی که روش کلیک شده رو گرفتم، و در صفحه‌ای که قراره آیتم‌های فرزند رو اضافه کنم و نمایش بدم، این id رو به عنوان parentId برای گره فرزند (todoی فرزند) قرار دادم و ذخیره می‌کنم...

ولی وقتی که روی آیتم پدر که کلیک می‌کنم تا بره صفحه بعد و todoهای فرزندش رو نشون بده، به مشکل برخوردم و دقیقا تمام آیتم هایی که وارد کردم رو نشون میده، نه todoهای فرزندش رو...
و صفحه اول برنامه که فقط قراره todoهای پدر رو نشون بده، متاسفانه تمام todoها رو نشون میده...

که فکر کنم به خاطر این هستش که در ابتدا فیلدی به نام var parentId: Long ?=0 رو داخل فراگمنت تعریف کردم و با کلیک بر روی هر آیتم، وقتی میره تا فراگمنت فرزند رو نشون بده، میره و متد oncreteView رو که لود کنه، باز همون parentId پدر رو که در ابتدا مقدار صفر رو بهش دادم رو به viewModel میده و همه آیتم هایی که می‌خوام وارد کنم رو با همین parentId ذخیره می‌کنه و برای همینم هست که همه رو با parentId صفر ذخیره می‌کنه و و داخل ریسایکلر ویو نشون میده...

که از اینجا به بعد رو واقعا تو پیاده سازیش به مشکل خوردم و واقعا نمی‌دونم چه جوری باید حلش کنم که در هر صفحه فرزند بتونم لیست فرزندان رو داشته باشم و ذخیره کنم

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

که می‌خواستم ازتون عاجزانه خواهش کنم که میشه کمی راهنماییم کنین...

و در پایان هم ازتون بسیار ممنون و سپاسگزارم که زمانی رو برای خوندن این نوشته اختصاص دادین...
ممنون و سپاسگزارم ازتون...

mehdi.safavie
دوشنبه 03 شهریور 1399, 12:46 عصر
درود;

امیر رضا جان، اول باید کد آداپتر اللخصوص کدی که روی آیتم میزنی رو بفرستی تا ببینیم چکار کردی.
چون چیزی که بهش نیاز داری انقدر ها که براش توضیح دادی پیچیده نیست.

اما، یه راهنمایی ساده میگم، اگر جواب نداد باید مارو با کد روشن کنی چه کردی، و چه اتفاقی افتاده، و نیاز داری چه انجام بدی.
زمانی که Fragment رو Load میکنی تو OnCreateView همونطور که گفتی ، متغییر
parentId رو 0 کن. اما بعد از این که هر آیتمی رو Touch کردی، نباید به هیچ وجه رویداد OnCreateView اتفاق بیافته، اگر داره اینطور میشه داری اشتباه میری، تنها باید لیست و آداپتر فیلتر بشه یه لیستی جدید.
حالا این که لیست رو چطور جدید میکنی هم، اول آیدی اون آیتمی که روش Touch کردی رو باید بگیری تحت عنوان parentId جستجو بزنی و لیست Child ها رو بگیری. در نهایت لیستی که بدست اومده رو بدی به آداپتر و NotifyChange بزنی به آداپترت.
اینطوری فقط داری لیست رو Refresh میکنی با محتویات جدید. حالا این که OnCreateView داره دوباره اتفاق میافته، یک جای کار شما به جای Refresh کردن لیست، داری Fragment رو دستکاری میکنی.

اگر اشتباه متوجه شدم، کد هایی که گفتم رو بفرستین ببینیم چی شده دقیقا.

mehdi.safavie
دوشنبه 03 شهریور 1399, 12:53 عصر
برای برگشت هم باید به گزینه برگشت روی گوشی کد بدی.
برای اینکار باید مکانیزم اون گزینه رو False کنی.
ببین یه راهش اینیه که گفتم، که مکانیزمش رو False کنی و اگر کاربر برگشت رو زد برگرده و parentId از parentId رو بگیره و لیست رو نمایش بده. ( یعنی ردیف اول که parentId 0 دارن رو روش Touch کردی، و در ردیف دومی هستی که همه parentId مثلا 1 رو دارن، اینجا اگر کاربر برگشت رو زد، باید اول parentId رو بگیره، تا به ردیف اول و آیدی 1 برسه، همچنان باید دوباره parentId رو بگیره که 0 هست، حالا لیست رو بر اساس parentId های 0 دوباره تولید کنه ).

همچنین، برای اینکه کاربر بتونه خروج هم داشته باشه، یا نهایت به یه Fragment دیگه برگشت کنه، باید از مکانیزم برای برگشت 2 بار از این گزینه استفاده کنید پیاده کنی.


@Override
public boolean handleOnBackPressed() {
//Do your job here
return true;
}


اون return اگر true باشه، کار اصلی خودش رو انجام میده، اگر false بزاری، هرچ کاری نمیکنه و فقط کد شما رو اجرا میکنه.

amirreza_dq
دوشنبه 03 شهریور 1399, 13:07 عصر
درود;

امیر رضا جان، اول باید کد آداپتر اللخصوص کدی که روی آیتم میزنی رو بفرستی تا ببینیم چکار کردی.
چون چیزی که بهش نیاز داری انقدر ها که براش توضیح دادی پیچیده نیست.

اما، یه راهنمایی ساده میگم، اگر جواب نداد باید مارو با کد روشن کنی چه کردی، و چه اتفاقی افتاده، و نیاز داری چه انجام بدی.
زمانی که Fragment رو Load میکنی تو OnCreateView همونطور که گفتی ، متغییر
parentId رو 0 کن. اما بعد از این که هر آیتمی رو Touch کردی، نباید به هیچ وجه رویداد OnCreateView اتفاق بیافته، اگر داره اینطور میشه داری اشتباه میری، تنها باید لیست و آداپتر فیلتر بشه یه لیستی جدید.
حالا این که لیست رو چطور جدید میکنی هم، اول آیدی اون آیتمی که روش Touch کردی رو باید بگیری تحت عنوان parentId جستجو بزنی و لیست Child ها رو بگیری. در نهایت لیستی که بدست اومده رو بدی به آداپتر و NotifyChange بزنی به آداپترت.
اینطوری فقط داری لیست رو Refresh میکنی با محتویات جدید. حالا این که OnCreateView داره دوباره اتفاق میافته، یک جای کار شما به جای Refresh کردن لیست، داری Fragment رو دستکاری میکنی.

اگر اشتباه متوجه شدم، کد هایی که گفتم رو بفرستین ببینیم چی شده دقیقا.



درود جناب صفوی...
ممنون که لطف کردین و این پست رو خوندین و جواب دادین...

واقعا ازتون ممنونم...

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

این کلاس آداپترم هستش:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.mytodoapp.R
import com.example.mytodoapp.data.db.models.TodoItem
import com.example.mytodoapp.ui.todolist.OnClickRecyclerL istener
import com.example.mytodoapp.ui.todolist.TodoViewModel
import kotlinx.android.synthetic.main.todo_recycler_item. view.*

class TodoRecyclerViewItemAdapter(
var items: List<TodoItem>,
private val viewModel: TodoViewModel

) : RecyclerView.Adapter<TodoRecyclerViewItemAdapter.TodoViewHolder>() {

private lateinit var onClickRecyclerListener: OnClickRecyclerListener

fun setOnClickRecyclerListener(onClickRecyclerListener : OnClickRecyclerListener) {
this.onClickRecyclerListener = onClickRecyclerListener
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.todo_recycler_item, parent, false)

return TodoViewHolder(view)
}

override fun getItemCount(): Int {
return items.size
}

override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val todoItem = items[position]

holder.itemView.todoTextView.text = todoItem.todoItem

holder.itemView.setOnClickListener(View.OnClickLis tener {
val parenId = items.get(position)
onClickRecyclerListener.onRecyclerItemClicked(pare nId)
})

holder.itemView.todoItemDeleteImageButton.setOnCli ckListener {
viewModel.delete(todoItem)
}

}

inner class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)


}



این هم اینترفیس برای رویداد کلیک ریسایکلر ویو
interface OnClickRecyclerListener {

fun onRecyclerItemClicked(todoItem: TodoItem)

}



اینترفیس مربوط به رویداد کلیک بر روی fab ونمایش یک دیالوگ واضافه کردن آیتم به ریسایکلر ویو:


interface AddDialogListener {

fun onAddButtonClicked(item: TodoItem)

}



کلاس مربوط به رویداد کلیک بر روی fab و اضافه کردن آیتم به ریسایکلر ویو:

class AddTodoItemDialog(context: Context, var parentId: Long, var addDialogListener: AddDialogListener) :
AppCompatDialog(context) {

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

tvSaveTodo.setOnClickListener {
val todo = etInputTodo.text.toString()

if (todo.isEmpty()) {
Toast.makeText(context, "Please Enter Your Todo!", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}

val item = TodoItem(todo, parentId)
addDialogListener.onAddButtonClicked(item)
dismiss()
}

tvCancel.setOnClickListener {
cancel()
}
}
}



و در انتها این هم کلاس فراگمنتم هستش:


import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.mytodoapp.R
import com.example.mytodoapp.data.db.TodoDatabase
import com.example.mytodoapp.data.db.models.TodoItem
import com.example.mytodoapp.data.repositories.TodoReposi tory
import com.example.mytodoapp.other.TodoRecyclerViewItemAd apter
import kotlinx.android.synthetic.main.fragment_todo.*


class TodoFragment : Fragment() {

var parentId: Long ?=0

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_todo, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val database = TodoDatabase(requireContext())
val repository = TodoRepository(database)
val factory = TodoViewModelFactory(repository)

val viewModel = ViewModelProvider(this, factory).get(TodoViewModel::class.java)

val adapter = TodoRecyclerViewItemAdapter(listOf(), viewModel)
todoRecyclerView.layoutManager = LinearLayoutManager(requireContext())
todoRecyclerView.adapter = adapter

parentId?.let {
viewModel.getChildTodoItems(it).observe(viewLifecy cleOwner, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
}

fabAddTodoItem.setOnClickListener {
parentId?.let { it1 ->
AddTodoItemDialog(requireContext(), it1, object : AddDialogListener {
override fun onAddButtonClicked(item: TodoItem) {
viewModel.upsert(item)
Toast.makeText(context, "Parent id is: $parentId", Toast.LENGTH_SHORT).show()
}
}).show()
}
}
adapter.setOnClickRecyclerListener(object : OnClickRecyclerListener {
override fun onRecyclerItemClicked(todoItem: TodoItem) {
parentId = todoItem.id
Toast.makeText(context, "parent id is:: $parentId", Toast.LENGTH_SHORT).show()
findNavController().navigate(
R.id.action_todoFragment_self
)

parentId?.let {
viewModel.getChildTodoItems(it).observe(viewLifecy cleOwner, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
}
}

})

}
}




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

mehdi.safavie
دوشنبه 03 شهریور 1399, 13:38 عصر
1. در onViewCreated از TodoFragment باید لیستی از تمام رکورد هایی که parentId برابر با 0 دارن رو بگیرین و به adapter بدین.
2. داخل adapter در قسمت onCreateViewHolder باید متدی تعریف کنید که با Touch روی هر آیتم، این متد فراخوانی بشه. برای اینکار راه های مختلفی هست، یه راهش استفاده از Extension هست.
میتونین همچین چیزی بنویسین:

fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
itemView.setOnClickListener {
event.invoke(getAdapterPosition(), getItemViewType())
}
return this
}

و از اون تو adapter استفاده کنین.
در قسمتی که گفتم میتونین اینطوری ازش استفاده کنین:

override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): MyViewHolder? {
val inflater = LayoutInflater.from(parent!!.getContext())
val view = inflater.inflate(R.layout.item_view, parent, false)
return MyViewHolder(view).listen { pos, type ->
val item = items.get(pos)
//TODO do other stuff here
}
}

بسیار خب؛ شما در قسمت return تمام item رو در اختیار دارین. یعنی میتونین به راحتی به فیلد های آبجکت خودتون دسترسی داشته باشین ( در صورتی که فیلد در رکورد مور نظر فقط در اختیارتون هست و این کار از طریق pos برای شما امکان پذیر شده ). دقیقا در همین قسمت شما میبایست لیست adapter رو به روزرسانی کنین. یعنی اینکه با توجه به Id که از item میگیرین، یک کوئری به دیتابیس بزنین و تمام child هایی که parentId اونها برابر با id بدست اومده هست رو لیست کنین.
لیست بدست اومده رو بدین به adapter. که در اصل باید این 3 خط کد رو بنویسین:

items.clear();
items.addAll(newList);
notifyDataSetChanged();



---------------------------------------------
اون extension که براتون گذاشتم فقط یه مثاله، درگیرش نشین، فقط کافیه تو اون قسمت از adapter بتونین OnClickListener رو کنترل کنین.
شما خودتون تو این خط که نوشتین این کارو کردین:

adapter.setOnClickRecyclerListener(object : OnClickRecyclerListener {

تنها کاری که باید بکنین اینه که لیست adapter رو دوباره با اطلاعات جدید پر کنین.

amirreza_dq
دوشنبه 03 شهریور 1399, 13:43 عصر
1. در onViewCreated از TodoFragment باید لیستی از تمام رکورد هایی که parentId برابر با 0 دارن رو بگیرین و به adapter بدین.
2. داخل adapter در قسمت onCreateViewHolder باید متدی تعریف کنید که با Touch روی هر آیتم، این متد فراخوانی بشه. برای اینکار راه های مختلفی هست، یه راهش استفاده از Extension هست.
میتونین همچین چیزی بنویسین:

fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
itemView.setOnClickListener {
event.invoke(getAdapterPosition(), getItemViewType())
}
return this
}

و از اون تو adapter استفاده کنین.
در قسمتی که گفتم میتونین اینطوری ازش استفاده کنین:

override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): MyViewHolder? {
val inflater = LayoutInflater.from(parent!!.getContext())
val view = inflater.inflate(R.layout.item_view, parent, false)
return MyViewHolder(view).listen { pos, type ->
val item = items.get(pos)
//TODO do other stuff here
}
}

بسیار خب؛ شما در قسمت return تمام item رو در اختیار دارین. یعنی میتونین به راحتی به فیلد های آبجکت خودتون دسترسی داشته باشین ( در صورتی که فیلد در رکورد مور نظر فقط در اختیارتون هست و این کار از طریق pos برای شما امکان پذیر شده ). دقیقا در همین قسمت شما میبایست لیست adapter رو به روزرسانی کنین. یعنی اینکه با توجه به Id که از item میگیرین، یک کوئری به دیتابیس بزنین و تمام child هایی که parentId اونها برابر با id بدست اومده هست رو لیست کنین.
لیست بدست اومده رو بدین به adapter. که در اصل باید این 3 خط کد رو بنویسین:

items.clear();
items.addAll(newList);
notifyDataSetChanged();



---------------------------------------------
اون extension که براتون گذاشتم فقط یه مثاله، درگیرش نشین، فقط کافیه تو اون قسمت از adapter بتونین OnClickListener رو کنترل کنین.
شما خودتون تو این خط که نوشتین این کارو کردین:

adapter.setOnClickRecyclerListener(object : OnClickRecyclerListener {

تنها کاری که باید بکنین اینه که لیست adapter رو دوباره با اطلاعات جدید پر کنین.


واقعا ازتون بینهایت ممنون و سپاسگزارم که تا این راخنماییم کردین...

خیلی خیلی ازتون ممنونم...
واقعا دستون درد نکنه...

ممنونم ازتون... همین الان انجامش میدم...

بازم ممنونم ازتون...