I'm trying to DRY up my code and I have a couple activities which use the same blocks of code which I want to move into a method in the parent activity. The problem is that this code uses generated ViewBindings which are unique classes, and I can't figure out what the parent class is in order to use it as a method parameter.
For example, this code is in two different activities and the only difference is that in one activity binding = Activity1Binding, in the other one it's Activity2Binding. They share some views with the same IDs.
binding.noteTitleTV.setOnClickListener { changeTitle() }
binding.deleteModalLayout.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.cancelDeleteButton.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.confirmDeleteButton.setOnClickListener { onDeleteNoteClicked() }
I would like to implement something like this in the parent activity to prevent duplicate code, if that's possible:
fun setUp(binding: [BINDING PARENT CLASS]) {
binding.noteTitleTV.setOnClickListener { changeTitle() }
// etc
}
The generated classes extend the Object class (java.lang.Object)
The binding class inherits from ViewDataBinding, so you could do this (Kotlin code)
fun setUp(binding: ViewDataBinding) {
when(binding){
is Activity1Binding -> { (binding as Activity1Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
is Activity2Binding -> { (binding as Activity2Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
}
// etc
}
I don't know that it could get more "generic" than that as you don't control the generated classes. But that would at least allow you to place all the code in a single class as you suggested. I use a similar approach in that I have an lateinit instance of all of my generated binding classes and just set which is active based on the passed variable and use that instance name so i don't have to keep typing as.
ex:
private lateinit var itemBinding : GroceryItemBinding
private lateinit var maintItemBinding : GroceryItemMaintBinding
private lateinit var compareItemBinding : GroceryItemCompareBinding
private lateinit var priceItemBinding : GroceryItemPriceBinding
private lateinit var adItemBinding : GroceryItemAdBinding
when(viewBinding){
is GroceryItemMaintBinding -> {
maintItemBinding = viewBinding as GroceryItemMaintBinding
maintItemBinding.groceryItem = gi
maintItemBinding.groceryItemImage.setOnClickListener { ... }
......
}
is GroceryItemBinding -> {
itemBinding = viewBinding as GroceryItemBinding
}
......
}
ViewBinding can create by bind(view), so you can create a base class like this:
abstract class BaseActivity : AppCompatActivity() {
private lateinit var binding: Activity1Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = createContentView()
// create ViewBinding
binding = Activity1Binding.bind(view)
}
// create view by subclass
abstract fun createContentView(): View
fun setTextViewTitle(text: CharSequence) {
binding.tvTitle.text = text
}
}
this is the content of Activity1Binding#bind():
#NonNull
public static ActivityMainBinding bind(#NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.layout;
FinanceLabelLayout layout = rootView.findViewById(id);
if (layout == null) {
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, layout);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
But this is not recommended.
This is not type safe.
Related
So I'm using AirBNB Epoxy (the view holders version) and I can't seem to find the information where I can set onClickListeners in AirBNB Epoxy. Any information helps, Thanks.
According to epoxy's document:
#EpoxyModelClass(layout = R.layout.model_button)
abstract class ButtonModel : EpoxyModelWithHolder<ButtonModel.Holder>() {
// Declare your model properties like this
#EpoxyAttribute
#StringRes
var text = 0
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
// Implement this to bind the properties to the view
holder.button.setText(text)
holder.button.setOnClickListener(clickListener)
}
class Holder : EpoxyHolder() {
lateinit var button: Button
override fun bindView(itemView: View) {
button = itemView.findViewById(R.id.button)
}
}
}
The generated model should be instantiated directly, and has a setter for each property:
ButtonModel_()
.id(1)
.text(R.string.my_text)
.clickListener { view -> /* do something */ }
Using Epoxy Library first time. Getting bellow error
rocess: in.droom, PID: 25269
com.airbnb.epoxy.IllegalEpoxyUsage: You must set an id on a model before adding it. Use the #AutoModel annotation if you want an id to be automatically generated for you.
Here is the controller code:
class MyMatchesController : EpoxyController() {
override fun buildModels() {
for (i in 0 until 10) {
fillBestMatchesNotification {
id(i)
bestMatchesCount("100")
}
}
}
}
Here is the Model code
#EpoxyModelClass(layout = R.layout.fill_best_match_notification_layout)
abstract class FillBestMatchesFormNotificationModel : EpoxyModelWithHolder<FillBestMatchesFormNotificationModel.ViewHolder>() {
#EpoxyAttribute
var id: Long = 0
#EpoxyAttribute
var bestMatchesCount = ""
override fun getDefaultLayout() = R.layout.fill_best_matches_notification_layout
#CallSuper
override fun bind(holder: ViewHolder) {
super.bind(holder)
holder.countTV.text = bestMatchesCount
}
override fun createNewHolder(): ViewHolder {
return ViewHolder()
}
inner class ViewHolder : EpoxyHolder() {
lateinit var countTV: TextView
override fun bindView(itemView: View) {
countTV = itemView.findViewById(R.id.matchedCountNotificationTV)
}
}
}
I have tried to delete the id from the Model class but still same error.
id is a reserved attribute and must not be declared as a new field with #EpoxyAttribute.
Just define your required attributes. id setters (with different overloads) will generate automatically.
so remove the below lines. everything will work well.
#EpoxyAttribute
var id: Long = 0
I am new to kotlin and i am trying to pass a value of checked radio button from one class to another activity through interface. I have an interface named RadioGroupHelperInterface as
interface RadioGroupHelperInterface {
fun onSelect(selectedItem: String)
}
Then i have a class from where i want to pass the value of checked radio button.
class GRadioGroupHelper {
private val radioGroupHelperInterface: RadioGroupHelperInterface? = null
fun setRadioExclusiveClick(parent: ViewGroup?) {
val radios: List<RadioButton>? = parent?.let { getRadioButtons(it) }
if (radios != null) {
for (radio in radios) {
radio.setOnClickListener { v ->
val r: RadioButton = v as RadioButton
r.isChecked = true
radioGroupHelperInterface?.onSelect(r.text as String)
checkedValue = r.text as String
for (r2 in radios) {
if (r2.getId() !== r.getId()) {
r2.isChecked = false
}
}
}
}
}
}
}
Finally my activity is as follows:
class ChooseCategoryActivity : AppCompatActivity(), View.OnClickListener,RadioGroupHelperInterface {
var radioGRadioGroupHelper=GRadioGroupHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_choose_category)
setListener()
val parent: ViewGroup = findViewById(R.id.svCategories)
radioGRadioGroupHelper.setRadioExclusiveClick(parent)
}
override fun onSelect(selectedItem: String) {
Log.e("Here","reached")
Log.e("value",selectedItem)
Toast.makeText(this,selectedItem,Toast.LENGTH_LONG).show()
}
}
But i am not able to get the value that i have checked in the radio box from the activity though the value can be printed in the RadioGRadioGroupHelper class. Can anybody please help me?
You don't set radioGroupHelpedInterface field to any value except for null, which is its initial state. Why don't you try this:
Declare your GRadioGroupHelper as following:
class GRadioGroupHelper (private val helperInterface: RadioGroupHelperInterface) {
// All your logic remains the same
}
This will allow you to avoid nullability of the RadioGroupHelperInterface instance and you will also be able to set it via constructor like this in the activity:
val radioGRadioGroupHelper = GRadioGroupHelper(this)
Note that I changed var to val as we don't expect your radioGRadioGroupHelper to change.
I have a MutableLiveData list in my repository as follows :
class AnswerRepository {
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
An item is added in 'answerList' (the MutableLiveData List) in a service as given below :
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
#SuppressLint("InflateParams")
override fun onCreate() {
super.onCreate()
//Getting the widget layout from xml using layout inflater
mFloatingView = LayoutInflater.from(this).inflate(R.layout.floating_widget, null)
initialiseViews()
setListeners()
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.next -> {
addItemInList()
}
}
private fun addItemInList(){
val answer = Answer(questionNumber, selectedOption, questionStatus)
answerRepository.addAnswerInList(answer)
}
Then this MutableLiveData List (answersList) is being observed in the fragment using viewmodel between repository and the fragment as follows :
ViewModel :
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
val answerList = answerRepository.returnAnswerList()
}
Fragment :
class SendAnswerToCloud : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentSendDataToCloudBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_send_data_to_cloud,
container,
false
)
binding.lifecycleOwner = this
viewModel.answerList.observe(viewLifecycleOwner, Observer {
Log.e("SendAnswerToCloud", "isChangeTrigerred")
val answer = viewModel.answerList.value?.last()
Log.e(
"SendAnswerToCloud",
"QuestionNumber : ${answer?.questionNumber}, SelectedOption : ${answer?.selectedOption}, QuestionStatus : ${answer?.questionStatus}"
)
})
return binding.root
}
}
The list is successfully updated in the repository when addAnswerInListis called in the service. However nothing happens in the fragment (as the logs don't appear in the logcat).
So, what am I doing wrong ? Any kind of help would be highly appreciated. Thanks in Advance!!
The problem in your implementation is that you instantiate two AnswerRepository objects instead of one. Thus, you get two var answerList = MutableLiveData<MutableList<Answer>>() instead of one. While your SendAnswerToCloud to cloud listening for changes on the first answerList your service edits the other answerList. That is the reason you do not see any changes.
Make sure you create only one AnswerRepository object.
In programming people use dependency injection and optionally in combination with singleton pattern. Sometimes you can get away using only singleton pattern, but this is a less flexible solution and not so easy to test.
Detailed reponse
So the problem occurs because you have first object instantiation in FloatingWidgetService class:
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
...
and the second instantiation in SendAnswerToCloudViewModel class:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
...
}
This way you create two absolutely separate objects. Each one of them occupies different address in memory, and all of the objects you create inside AnswerRepository are also different between these two instances.
Imagine placing those declarations one after the other like this:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
private val answerRepository_second = AnswerRepository()
...
}
If you later compare them by equals method or by == operator you will get result false because they are two different objects. Thus, if you set a new value to answerList of answerRepository subscribers of answerList stored in answerRepository_second will not receive any updates.
Solution
Add companion object and make the primary constructor private.
class AnswerRepository private constructor() {
companion object {
private var INSTANCE: AnswerRepository? = null
fun getInstance(): AnswerRepository {
if (INSTANCE == null) {
INSTANCE = AnswerRepository()
}
return INSTANCE!!
}
}
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
Now instead of writing declarations with constructor invocation:
private val answerRepository = AnswerRepository()
You will call getInstance() method to get AnswerRepository.
private val answerRepository = AnswerRepository.getInstance()
This pattern is called singleton. When you ensure that your program has only one instance of a specific class.
Strongly recommend you to complete Essentials and Kotlin maps here.
I am new at Kotlin and trying to implement MVP Architecture,
Currently I am having problem initializing/setting textview's value outside onCreate() method
here is my code
SplashActivity.kt
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
lateinit var appDetail: AppDetail
lateinit var textTitle: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textTitle = findViewById(R.id.splash_txt_title) as TextView
AppSingleton.appContext = applicationContext
var splashPresentation = SplashPresentation(this)
splashPresentation.getAppDetailFromService()
}
override fun fetchAppDetailSuccessful(response: SplashServiceObject) {
AppSingleton.initializeAppDetal(Gson().fromJson(response.json_string, AppDetail::class.java))
this.appDetail = AppSingleton.appDetail
}
override fun fetchAppDetailFailed(errMsg: String) {
textTitle.text = errMsg
}
}
SplashPresenter.kt
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
fun getAppDetailFromService() {
var splashService = SplashService()
splashService.getAppDetailFromAssets(this)
}
override fun fetchAppDetailFromServiceSuccessful(response: SplashServiceObject) {
view.fetchAppDetailSuccessful(response)
}
override fun fetchAppDetailFromServiceFailed(errMsg: String) {
view.fetchAppDetailFailed(errMsg)
}
}
SplashService.kt
class SplashService {
fun getAppDetailFromAssets(splashPresentation: SplashPresentation) {
val json_filename = "appdetail.json"
var jsonResponse: JsonResponse = AppSingleton.commonUtils.fetchJsonFromAssets(json_filename, AppSingleton.appContext!!)
if (jsonResponse.json_status) {
var splashServiceObj = SplashServiceObject
splashServiceObj.json_string = jsonResponse.json_info
splashServiceObj.response_msg = "JSON Successful fetched."
splashPresentation.fetchAppDetailFromServiceSuccessful(splashServiceObj)
} else {
splashPresentation.fetchAppDetailFromServiceFailed(jsonResponse.json_info)
}
}
}
in my SplashActivity().onCreate(), I am calling a Presenter that access Service, then the Service return a value to Presenter,
Then Presenter, return value to my SplashActivity's View, one of the function is, fetchAppDetailFailed(errMsg)
when I run the app, it crashes, saying the "textaa" is not yet initialized.
back in Java exp, when the variable is already instantiated on onCreate(), you can call this variable anywhere within the activity.
Thanks in advance!
You cannot instantiate Activities on Android. They are instantiated by the OS, and the OS calls the lifecycle methods on it.
In an MVP pattern, the View and Presenter both reference each other. Since Activity (the View) is the entry point of the application, your Activity should instantiate the Presenter and pass a reference of itself to the Presenter so communication can go both ways.
Also, the reference to the activity in the Presenter should be specified as a ViewInterface, not an Activity, or you're kind of defeating the purpose of using MVP.
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
//... methods that call functions on view
}
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
private val presenter = SplashPresentation(this)
//...
}