10

I based my code on an example I found that uses Android Architecture Components and data binding. This is a new way for me, and the way it is coded makes it hard to properly open a new activity with the information of the post that was clicked.

This is the adapter of the posts

class PostListAdapter : RecyclerView.Adapter<PostListAdapter.ViewHolder>() {
    private lateinit var posts: List<Post>

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostListAdapter.ViewHolder {
        val binding: ItemPostBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_post,
            parent, false
        )

        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: PostListAdapter.ViewHolder, position: Int) {
        holder.bind(posts[position])
    }

    override fun getItemCount(): Int {
        return if (::posts.isInitialized) posts.size else 0
    }

    fun updatePostList(posts: List<Post>) {
        this.posts = posts
        notifyDataSetChanged()
    }

    inner class ViewHolder(private val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root) {
        private val viewModel = PostViewModel()

        fun bind(post: Post) {
            viewModel.bind(post)
            binding.viewModel = viewModel
        }
    }
}

The bind method comes from within the view model class:

class PostViewModel : BaseViewModel() {
    private val image = MutableLiveData<String>()
    private val title = MutableLiveData<String>()
    private val body = MutableLiveData<String>()

    fun bind(post: Post) {
        image.value = post.image
        title.value = post.title
        body.value = post.body
    }

    fun getImage(): MutableLiveData<String> {
        return image
    }

    fun getTitle(): MutableLiveData<String> {
        return title
    }

    fun getBody(): MutableLiveData<String> {
        return body
    }

    fun onClickPost() {
        // Initialize new activity from here, perhaps?
    }
}

And in the layout XML, setting on an onClick attribute

android:onClick="@{() -> viewModel.onClickPost()}"

pointing to this onClickPost method does work but I can't initialize the Intent from there. I tried many ways to acquire the MainActivitiy's context, without success, such as

val intent = Intent(MainActivity::getApplicationContext, PostDetailActivity::class.java)

But it displays an error on time.

  • 2
    The ViewModel is NOT supposed to be aware of the Context or anything about Android. So I guess the view needs to subscribe to an event or something emitted by the ViewModel when the onClickPost method is called. However I'm facing a similar issue so I'm interested in a proper answer. – Eselfar Aug 27 '18 at 04:10
  • 1
    Try the [singleliveevent](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150) pattern – MidasLefko Aug 27 '18 at 05:17
  • @MidasLefko, seems it could be it, but I am having problems with the ViewModelFactory not being flexible and dynamic accepting more than one type of ViewModel. –  Aug 27 '18 at 05:50
  • @gamofe that sounds like a new question.. . – MidasLefko Aug 27 '18 at 06:50
  • It dies. I did create a new one https://stackoverflow.com/questions/52033403/how-to-make-this-viewmodelfactory-more-flexible-and-accept-different-kinds-of-vi –  Aug 27 '18 at 06:55
  • @MidasLefko You should write an answer as it seems to be the correct approach. – Eselfar Aug 27 '18 at 07:07

3 Answers3

4

Try using a SingleLiveEvent

Here is the code for it from Googles architecture samples repo (in case it ever gets removed from the repo):

import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 * <p>
 * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * <p>
 * Note that only one observer is going to be notified of changes.
 */
public class SingleLiveEvent<T> extends MutableLiveData<T> {

    private static final String TAG = "SingleLiveEvent";

    private final AtomicBoolean mPending = new AtomicBoolean(false);

    @MainThread
    public void observe(LifecycleOwner owner, final Observer<T> observer) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
        }

        // Observe the internal MutableLiveData
        super.observe(owner, new Observer<T>() {
            @Override
            public void onChanged(@Nullable T t) {
                if (mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
            }
        });
    }

    @MainThread
    public void setValue(@Nullable T t) {
        mPending.set(true);
        super.setValue(t);
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    public void call() {
        setValue(null);
    }
}
MidasLefko
  • 4,499
  • 1
  • 31
  • 44
3

Try: android:onClick="@{(view) -> viewModel.onClickPost(view)}"

Also change onClickPost to take in a View. Then you can use the view.getContext() method on the view to get access to the Context stored in that view.

However, since ViewModels shouldn't reference a view or any other class that holds an Activity's context, it's quite inappropriate to place your logic for starting an Activity in the ViewModel. You should definitely consider a separate place to do so.

Personally, for my code, if it's a simple startActivity without any extra baggage, I create a separate class that holds a static method. Through databinding, I'll import that class and use it in the onClick to start a new Activity using the method I said above.

An example of this:

public class ActivityHandler{        
    public static void showNextActivity(View view, ViewModel viewModel){
        Intent intent = new Intent(); //Create your intent and add extras if needed
        view.getContext().startActivity(intent);
    }
}

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="whatever.you.want.ActivityHandler" />
        <variable name="viewmodel" type="whatever.you.want.here.too.ViewModel" />
    </data>

    <Button
        //Regular layout properties
        android:onClick="@{(view) -> ActivityHandler.showNextActivity(view, viewmodel)}"
        />
</layout>

Look at Listener Bindings here: https://developer.android.com/topic/libraries/data-binding/expressions#listener_bindings

However, depending on the amount of data necessary, you might want to place your startActivity code in other classes that best fits your app's design.

Jackey
  • 3,184
  • 1
  • 11
  • 12
  • Is it not breaking the MVVM pattern doing so? – Eselfar Aug 27 '18 at 04:17
  • Also, `startActivity()` isn't available within the view model. –  Aug 27 '18 at 04:19
  • @Eselfar Yep, I was in the process of editing my answer to say exactly that. – Jackey Aug 27 '18 at 04:28
  • @gamofe it's possible to use `startActivity()` through the context you get from the View object. `view.getContext().startActivity()` – Jackey Aug 27 '18 at 04:29
  • Can you show a codebase with the most appropriate way? –  Aug 27 '18 at 05:04
  • In my case, at least in the beginning, I'd like to put extra strings for the post title, body, and dates. –  Aug 27 '18 at 05:14
  • @gamofe I edited my answer with a rough example. You can pass in other things like the ViewModel so you can use it to add as intent extras for the new Activity. – Jackey Aug 27 '18 at 05:23
  • you should not use context in Viewmodel it will break the mvvm pattern and create run time crashes all the context related things should be done in View – Rohit Sharma Aug 27 '18 at 06:47
  • 1
    @RohitSharma I did not say to use context in ViewModel. I explicitly said ViewModels shouldn't use it and offered an alternative solution to avoid using it in ViewModel. – Jackey Aug 27 '18 at 07:11
3

You can even pass activity instance to the model or layout, but I will not prefer that.

Preferred way is to pass interface to the row layout.

declare variable in layout data

<variable
    name="onClickListener"
    type="android.view.View.OnClickListener"/>

invoke this when clicked

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{onClickListener::onClick}"
    >

also set this listener from adapter

 binding.viewModel = viewModel
 binding.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            context.startActivity(new Intent(context, MainActivity.class));
        }
    });
Khemraj Sharma
  • 57,232
  • 27
  • 203
  • 212