https://medium.com/@shashankmohabia/android-paging-efficient-way-to-populate-recycler-view-dynamically-31f39f35cdf9

처음 개념 잡기 쉽게 자세히 설명이 되어 있다.

Paging Library Architecture:

image

The paging library consists of 5 components:

  • PagedList — The Paging Library’s key component is the PagedList class, which is a collection that loads chunks of your app’s data, or pages, asynchronously. It can be used to load data from sources you define, and present it easily in your UI with a RecyclerView. As more data is needed, it’s paged into the existing PagedList object. If any loaded data changes, a new instance of PagedList is emitted to the observable data holder from a LiveData or RxJava2-based object. As PagedList objects are generated, your app’s UI presents their contents, all while respecting your UI controllers’ lifecycles.
  • DataSource and DataSource.Factory — a DataSource is the base class for loading snapshots of data into a PagedList. A DataSource.Factory is responsible for creating a DataSource.
  • LivePagedListBuilder — builds a LiveData<PagedList>, based on DataSource.Factory and a PagedList.Config(the configuration file for paging).
  • BoundaryCallback — signals when a PagedList has reached the end of available data so that more data can be fetched from the server in case of DB backed up by the server.
  • PagedListAdapter — a RecyclerView.Adapter that presents paged data from PagedLists in a RecyclerView. PagedListAdapter listens to PagedList loading callbacks as pages are loaded and uses DiffUtil to compute fine-grained updates as new PagedLists are received.

Implementation:

Step 1. Load data in chunks with the PagedList:

To start using paging, replace all the List data to PagedList in the ViewModels and its observers in activities and fragments. If there are any models responsible for updating UI, change them also.

val dataList: LiveData<List<data_model_object>> = ...
val dataList: LiveData<PagedList<data_model_object>> = ...
viewModel.dataList.observe(this, Observer<PagedList<data_model_object>> {...})

Step 2. Define the source of data for the paged list:

The PagedList loads content dynamically from a source. If the database is the main source of truth for the UI, it also represents the source for the PagedList. If your app gets data directly from the network and displays it without caching, then the class that makes network requests would be your data source.

A source is defined by a DataSource class. To page in data from a source that can change—such as a source that allows inserting, deleting or updating data—you will also need to implement a DataSource.Factory that knows how to create the DataSource. Whenever the data is updated, the DataSource is invalidated and re-created automatically through the DataSource.Factory.

The Room persistence library provides native support for data sources associated with the Paging library. For a given query, Room allows you to return a DataSource.Factory from the DAO and handles the implementation of the DataSource for you.

@Daointerface RepoDao {
   @Query(<query>)
   fun someQueryMethod(): LiveData<List<data_model_object>>
   fun someQueryMethod(): DataSource.Factory<Int, data_model_object>...}

Step 3. Build and Config the paged list:

To build and configure a LiveData<PagedList>, use a LivePagedListBuilder.

//get the dataSourceFactory by making a call to dao function
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()

Besides the DataSource.Factory, you need to provide a PagedList configuration, which can include the following options:

  • The size of a page loaded by a PagedList
  • How far ahead to load
  • How many items to load when the first load occurs during the creation of pagedList by DataSource.
  • Whether null items can be added to the PagedList, to display placeholders for data that hasn’t been loaded yet. As soon as the data becomes available the ViewHolder gets populated with that and the view is updated with a nice cross-fade animation. If you have it disabled then your scroll bar jumps whenever data loads and that gives rise to bad user experience.
    But there are some problems with placeholders:
    a) All the items should be of the same size otherwise the animation looks weird.
    b) The adapter must handle null items.
    c) DataSource must count items (room does that implicitly).

You can have a separate config object to add configurations.

val config = PagedList.Config.Builder()
.setPageSize(30) //mandatory to provide
.setInitialLoadSizeHint(50) //default: page size*3
.setPrefetchDistance(10) //default: page size
.setEnablePlaceholder(true/false) //default: true
.build()val data = LivePagedListBuilder(dataSourceFactory, config).build()

Note: The DataSource page size should be several screens’ worth of items. If the page is too small, your list might flicker as pages content doesn’t cover the full screen. Larger page sizes are good for loading efficiency, but can increase latency when the list is updated.

Step 4. Make the RecyclerView Adapter work with a PagedList:

To bind a PagedList to a RecycleView, use a PagedListAdapter. The PagedListAdapter gets notified whenever the PagedList content is loaded and then signals the RecyclerView to update.

class RecyclerViewAdapter : PagedListAdapter<data_model_object, RecyclerView.ViewHolder>(DATA_COMPARATOR)

Also notice that now the data item is nullable and we have to update the onBindViewHolder to make it nullable. You can work with that data by applying checks if you want.

val item:data_model_object? = getItem(position)// optional        
if (item != null) {            
(holder as itemViewHolder).bind(item)        }

Step 5. Trigger network updates:

If your entire data is on a local database then we are done and you do not need to perform this step. But if your local DB is backed up by a server then we should find to way to ask for more data from the server only when there is no more data in the local DB. By providing all the data to the UI from the DB itself we follow the single source of truth principle.

For this, we use BoundaryCallback which gives two methods to perform some task when there are zero items in the list or the last available has been loaded so we need more data.

For this create a class extending the BoundaryCallback class and provide it the service and the local DB instances. Then override the methods to update the DB with your server data.

class sampleBoundaryCallback(
       private val service: ApiService,
       private val db: localDb,
       private val query:String
) : PagedList.BoundaryCallback<data_model_object>() {
   override fun onZeroItemsLoaded() {
        loadDataFromServerAndStoreInDB()}

   override fun onItemAtEndLoaded(itemAtEnd: data_model_object) {
        loadDataFromServerAndStoreInDB()    }
}

Then add the callback to LivePagedListBuilder:

val boundaryCallback = SampleBoundaryCallback(service, db, query)

   // Get the paged list
   val data = LivePagedListBuilder(dataSourceFactory, config)
            .setBoundaryCallback(boundaryCallback)
            .build()

Wrap up:

Now that we added all the components, let’s take a step back and see how everything works together.

The DataSource.Factory (implemented by Room) creates the DataSource. Then, LivePagedListBuilder builds the LiveData<PagedList>, using the passed-in DataSource.Factory, BoundaryCallback, and PagedList configuration. This LivePagedListBuilder object is responsible for creating PagedList objects. When a PagedList is created, two things happen at the same time:

  • The LiveData emits the new PagedList to the ViewModel, which in turn passes it to the UI. The UI observes the changed PagedList and uses its PagedListAdapter to update the RecyclerView that presents the PagedList data. (The PagedList is represented in the following animation by an empty square).
  • The PagedList tries to get the first chunk of data from the DataSource. When the DataSource is empty, for example when the app is started for the first time and the database is empty, it calls BoundaryCallback.onZeroItemsLoaded(). In this method, the BoundaryCallback requests more data from the network and inserts the response data in the database.
image

After the data is inserted in the DataSource, a new PagedList object is created (represented in the following animation by a filled-in square). This new data object is then passed to the ViewModel and UI using LiveData and displayed with the help of the PagedListAdapter.

image

When the user scrolls, the PagedList requests that the DataSource load more data, querying the database for the next chunk of data. When the PagedList paged all the available data from the DataSource, BoundaryCallback.onItemAtEndLoaded() is called. The BoundaryCallback requests data from the network and inserts the response data in the database. The UI then gets re-populated based on the newly-loaded data.

image

You can find a sample project implementing Paging here. I will also make a video tutorial for paging on youtube and will add the link here.

Feel free to comment if there are doubts or if you feel anything needs to be corrected😀.

Resources:

IO18’ Paging library launch

Android Codelab

Android Developers documentation

.

.

.

.

.

https://www.zoftino.com/pagination-in-android-using-paging-library

Setup

To include paging library in your project, you need to add below entry to build.gradle file.

def paging_version = "1.0.0"
implementation "android.arch.paging:runtime:$paging_version"

Since RecyclerView, Room, ViewModle and LiveData are used in the examples, we need to add following entries as well.

    implementation 'com.android.support:recyclerview-v7:28.0.0-rc01'

    def lifecycle_version = "1.1.1"
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"

    def room_version = "1.1.1"
    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version"

How It Works

A collection  (PagedList)  which acts as mediator between UI and Database uses data source to asynchronously fetch page data and populates itself with the loaded data.  (PagedList 는 UI와 데이터베이스 사이에 위치하며 data source를 이용하여 데이터를 가져온다. 새로 가져온 데이터를 가지고 PagedList 가 생성된다. )  The collection is passed to UI components thru listeners for displaying it in UI.

The collection or list is provided with configuration which contains initial load size and page size.  (PagedList 는 page관련 config정보를 가지며 page관련 정보도 관리한다)  The list manages page parameters and passes them to data source so that it can use it to load data according to configuration.

In response to UI events, UI component sends signal to the collection for next page.  (UI에서 더이상 보여줄 데이터가 없어진 경우 이를 PagedList 알리고 추가로 데이터를 가져오게 한다 ) The collection fetches data for next page and observer or listener passes the loaded data to UI component for displaying.

Paging Library Components

The collection mentioned above is    PagedList    class in paging library and it is created using    LivePagedListBuilder   (이를 통해 PagedList를 만든다 ). LivePagedListBuilder is supplied with data source factory and page list configuration objects.   (LivePagedListBuilder 만들때 전달해야 하는 parameters로는 data source factory, pagedlist의 config내용이 있다)

PagedList object is passed to recycler view adapter which can handle PagedList object. PagedList object is passed to recycler view by creating LiveData of PagedList.    (PagedList는 livedata형태로 adapater에 전달된다)   The observer of LiveData passes the PagedList object to the recycler view adapter which takes care of displaying data in RecyclerView, sending next page signal to PagedList and listening to PagedList for data updates.   (adapter는 다음 page를 위한 신호를 PagedList에 보내고 data update를 기대한다)

The important behavior of PagedList is that it can’t reload records if data is updated in the database. To show updates of already loaded data, new PagedList object has to be created.   (데이터가 업데이트된 경우 PagedList는 데이터를 reload하는것이 아니고 새로운 PagedList를 생성한다 )

DataSource.Factory    which is passed to    LivePagedListBuilder   is a factory for data source.    (DataSource.Factory는  data source를 생성하는 역할을 하며  LivePagedListBuilder에 전달된다)   You need to create DataSource.Factory and override create() method which returns DataSource object.   (DataSource.Factory의 create()를 override하고 이것의 리턴값이 DataSource object가 되게 해야한다)

DataSource    is used for loading data by PagedList. You can create DataSource by extending one of the three data source classes such as    PageKeyedDataSource, ItemKeyedDataSource, or PositionalDataSource.    (DataSource는 PagedList에 의해 데이터를 load하는 작업을 수행하며 PageKeyedDataSource, ItemKeyedDataSource, PositionalDataSource 중 하나를 extend한다)   You need to implement loadInitial and loadAfter methods. Using parameters in loadInitial and loadAfter methods, you can prepare query and load data.   (DataSource는 loadInitial, loadAfter 두 메소드를 implement한다 )

PageKeyedDataSource and ItemKeyedDataSource can be used to load data based on key and size. They differ the way last loaded key is captured and passed as parameter to next call.

PositionalDataSource interacts with source of data which can provide data at any position and loads required number of items.

PagedListAdapter    is a recycler view adapter for displaying data from PagedList. Initial data is passed to PagedListAdapter by calling submitList on the adapter and passing the list.

PagedListAdapter calls loadAround on PagedList object to make PagedList load data for next pages as user scrolls the items in recycler view. PagedListAdapter listens for data changes and displays the changes in recycler view.   (PagedListAdapter 는 다음페이지를 위해 PagedList object의 loadAround를 호출한다)

.

.

.

.

.

https://youtu.be/c_v0y2k-bww

10분까지 내용 개념 성명 좋음

image
image
image
image
image
image
image
image
image

.

.

.

.

.

실제 예제 코드

orginal source : https://www.zoftino.com/pagination-in-android-using-paging-library

DataSource Factory

You need to extend DataSource Factory class provided by paging library and implement create() method which returns DataSource.

import android.arch.paging.DataSource;
import android.content.Context;

/**
 * data source factory passed to PageList which calls create method to get
 * data source object
 */
public class CouponsDataSourceFactory extends DataSource.Factory<Integer, Coupon>  {
    private Context ctx;
    private CouponsDataSource couponsDataSource;
    
    public CouponsDataSourceFactory(Context ctx){
            this.ctx = ctx;
    }
    @Override
    public DataSource<Integer, Coupon> create() {
        if(couponsDataSource == null){
            couponsDataSource = new CouponsDataSource(ctx);
        }
        return couponsDataSource;
    }
}

DataSource

DataSource returns data to PageList. For our example, data source is created by extending PageKeyedDataSource class as we are going to use record id as key. You can use ItemKeyedDataSource as well. We need to override loadInitial() and loadAfter() methods of PageKeyedDataSource. In the data source, we use Room DAO to load data from the local database. In both the callack methods loadInitial() and loadAfter(), you can obtain the key from load parameters passed to them and pass the key to DAO to load data corresponding to the page being requested.

Our example initially loads records from 0 to 20 and then for next pages, it loads 25 records starting from the last loaded data.

import android.arch.paging.PageKeyedDataSource;
import android.content.Context;
import android.support.annotation.NonNull;

import java.util.List;

//data source for PagedList, it is used for loading data for each page
public class CouponsDataSource extends PageKeyedDataSource<Integer, Coupon> {
    private CouponLocalDAO couponDAO;

    public CouponsDataSource(Context ctx){
        couponDAO = LocalRepository.getCouponDB(ctx).couponDAO();
    }
    //is called too load initial data
    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params,
                            @NonNull LoadInitialCallback<Integer, Coupon> callback) {

        List<Coupon> cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);

        //this is required to handle first request after db is created or app is installed
        int noOfTryies = 0;
        while(cpns.size() == 0){
            cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);
            noOfTryies++;
            if(noOfTryies == 6){
                break;
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {  }
        }

        callback.onResult(cpns,null,
                cpns.size()+1);

    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params,
                           @NonNull LoadCallback<Integer, Coupon> callback) {

    }

    //is called to load pages of data using key passed in params
    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params,
                          @NonNull LoadCallback<Integer, Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize( params.key, params.requestedLoadSize);
        int nextKey = params.key+cpns.size();
        callback.onResult(cpns, nextKey);
    }
}

ViewModel

It contains view model factory which is need so that context can be passed to ViewModel as context is needed for initializing Room database.

In ViewModel constructor, data source factory defined above is instantiated and page list config object is created. Factory and config objects are passed to LivePagedListBuilder to build LiveData of PagedList object.

mport android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;

public class CouponViewModel extends ViewModel {
    //PagedList controls data loading using data source
    public LiveData<PagedList<Coupon>> couponList;

    public CouponViewModel(Application application){

        //instantiate CouponsDataSourceFactory
        CouponsDataSourceFactory factory = new CouponsDataSourceFactory(application);

        //create PagedList Config
        PagedList.Config config = (new PagedList.Config.Builder()).setEnablePlaceholders(true)
                                .setInitialLoadSizeHint(20)
                                .setPageSize(25).build();

        //create LiveData object using LivePagedListBuilder which takes
        //data source factory and page config as params
        couponList = new LivePagedListBuilder<>(factory, config).build();
    }

    //factory for creating view model,
    // required because we need to pass Application to view model object
    public static class CouponViewModelFactory extends ViewModelProvider.NewInstanceFactory {
        private Application mApplication;
        public CouponViewModelFactory(Application application) {
            mApplication = application;
        }
        @Override
        public <T extends ViewModel> T create(Class<T> viewModel) {
            return (T) new CouponViewModel(mApplication);
        }
    }
}

Recycler View Adapter PagedListAdapter

To display data in recycler view and interact with PagedList object, we need to create adapter class which extends PagedListAdapter.

You need to implement DiffUtil.ItemCallback and implement areItemsTheSame and areContentsTheSame methods. DiffUtil.ItemCallback object, which is used to compare data objects, is passed to super construction of your adapter class.

Initial data is passed to PagedListAdapter by calling submitList method on it. Internally it handles recycler view scroll events and sends the next page load signals to PagedList by calling loadAround on paged list object.

import android.arch.paging.PagedListAdapter;
import android.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/** this adapter displays coupon items in recycler view
 *  it extends PagedListAdapter which gets data from PagedList
 *  and displays in recycler view as data is available in PagedList
 */
public class CouponAdapter extends PagedListAdapter<Coupon, CouponViewHolder> {

    protected CouponAdapter() {
        super(DIFF_CALLBACK);
    }

    //DiffUtil is used to find out whether two object in the list are same or not
    public static DiffUtil.ItemCallback<Coupon> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Coupon>() {
        @Override
        public boolean areItemsTheSame(@NonNull Coupon oldCoupon,
                                       @NonNull Coupon newCoupon) {
            return oldCoupon.get_id() == newCoupon.get_id();
        }

        @Override
        public boolean areContentsTheSame(@NonNull Coupon oldCoupon,
                                          @NonNull Coupon newCoupon) {
            return oldCoupon.equals(newCoupon);
        }
    };

    @Override
    public CouponViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater li = LayoutInflater.from(parent.getContext());
        View view = li.inflate(R.layout.coupon_item, parent, false);
        return new CouponViewHolder(view);
    }

    @Override
    public void onBindViewHolder(CouponViewHolder holder, int position) {
        Coupon coupon = getItem(position);
        if(coupon != null) {
            holder.bindTO(coupon);
        }
    }
}

Activity

In the activity, create view model and adapter objects, set recycler view adapter and listen to live data object which exists in view model. In the handler, pass the paged list to adapter by calling the submitList method.

import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = findViewById(R.id.coupons_rv);

        CouponViewModel viewModel = ViewModelProviders.of(this,
                new CouponViewModel.CouponViewModelFactory(this.getApplication()))
                .get(CouponViewModel.class);

        CouponAdapter adapter = new CouponAdapter();
        recyclerView.setAdapter(adapter);

        //listen to data changes and pass it to adapter for displaying in recycler view
        viewModel.couponList.observe(this, pagedList -> {
            adapter.submitList(pagedList);
        });

        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        DividerItemDecoration dividerItemDecoration =
                new DividerItemDecoration(recyclerView.getContext(),
                        LinearLayoutManager.VERTICAL);
        recyclerView.addItemDecoration(dividerItemDecoration);
    }
}

Other Classes

Following are other classes created for this examples.

RecyclerView ViewHolder

public class CouponViewHolder extends RecyclerView.ViewHolder {
    public TextView storeNameTv;
    public TextView couponTv;

    public CouponViewHolder(View view) {
        super(view);
        storeNameTv = view.findViewById(R.id.coupon_store);
        couponTv = view.findViewById(R.id.coupon_tv);
    }

    public void bindTO(Coupon coupon){
        storeNameTv.setText(coupon.getStore());
        couponTv.setText(coupon.getOffer());
    }
}

Entity

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;

@Entity
public class Coupon {
    @PrimaryKey(autoGenerate = true)
    private int _id;
    private String store;
    private String offer;

    public Coupon(){}
    public Coupon(String store, String coupons){
        this.store = store;
        this.offer = coupons;
    }

    public String getStore() {
        return store;
    }

    public void setStore(String store) {
        this.store = store;
    }

    public int get_id() {
        return _id;
    }

    public void set_id(int _id) {
        this._id = _id;
    }

    public String getOffer() {
        return offer;
    }

    public void setOffer(String offer) {
        this.offer = offer;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        Coupon coupon = (Coupon)obj;
        return coupon.get_id() == this.get_id() &&
                coupon.getStore() == coupon.getStore() &&
                coupon.getOffer() == coupon.getOffer();
    }
}

Room DAO

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.Query;

import java.util.List;

@Dao
public interface CouponLocalDAO {
    //to fetch data required to display in each page
    @Query("SELECT * FROM Coupon WHERE  _id >= :id LIMIT :size")
    public List<Coupon> getCouponsBySize(int id, int size);

    //this is used to populate db
    @Insert
    public void insertCoupons(List<Coupon> coupons);
}


Room Database

import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;

@Database(entities = {Coupon.class}, version = 1)
public abstract class CouponsDB extends RoomDatabase {
    public abstract CouponLocalDAO couponDAO();
}

Repository

import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;

public class LocalRepository {
    private static CouponsDB couponDB;
    private static final Object LOCK = new Object();
    private static Context ctx;

    public synchronized static CouponsDB getCouponDB(Context context) {
        if (couponDB == null) {
            ctx = context;
            synchronized (LOCK) {
                if (couponDB == null) {
                    couponDB = Room.databaseBuilder(context,
                            CouponsDB.class, "Coupons Database")
                            .fallbackToDestructiveMigration()
                            .addCallback(dbCallback).build();

                }
            }
        }
        return couponDB;
    }

    private static RoomDatabase.Callback dbCallback = new RoomDatabase.Callback() {
        public void onCreate(SupportSQLiteDatabase db) {

            Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
                @Override
                public void run() {
                    addCoupons(ctx);
                }
            });
        }
    };

    private static void addCoupons(Context ctx){
        List<Coupon> couponList = new ArrayList<Coupon>();

        for(String s : initCoupons){
            String[] ss = s.split("\|");
            couponList.add(new Coupon(ss[0], ss[1]));
        }
        getCouponDB(ctx).couponDAO().insertCoupons(couponList);
    }
    private static String[] initCoupons = {"amazon|falt 20% off on fashion",
            "amazon|upto 30% off on electronics",
            "ebay|falt 20% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 30% off on fashion", "bestbuy|upto 80% off on electronics",
            "sears|falt 60% off on fashion", "ee|upto 40% off on electronics",
            "macys|falt 30% off on fashion", "alibaba|upto 90% off on electronics",
            "nordstorm|falt 90% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "jcpenny|falt 50% off on fashion", "ebay|upto 50% off on electronics",
            "khols|falt 70% off on fashion", "ebay|upto 40% off on electronics",
            "target|falt 30% off on fashion", "ebay|upto 20% off on electronics",
            "costco|falt 80% off on fashion", "ebay|upto 40% off on electronics",
            "walmart|falt 10% off on fashion", "ebay|upto 10% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "ebay|falt 40% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 70% off on fashion", "ebay|upto 80% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 60% off on fashion", "ebay|upto 50% off on electronics",
            "ebay|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "ebay|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "uuuu|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "tttt|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "ssss|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "eee|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "www|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "rrrr|falt 30% off on fashion", "tyyyy|upto 40% off on electronics",
            "vvvv|falt 30% off on fashion", "wwwwe|upto 40% off on electronics",
            "bbbb|falt 30% off on fashion", "ssssssssssssa|upto 40% off on electronics",
            "mmmm|falt 30% off on fashion", "rrtttt|upto 40% off on electronics",

    };
}

Activity Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
    android:id="@+id/coupons_rv"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>

Item Layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="8dp">

    <TextView
        android:id="@+id/coupon_store"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/coupon_tv"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <TextView
        android:id="@+id/coupon_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:textColor="@color/colorAccent"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        app:layout_constraintLeft_toRightOf="@+id/coupon_store"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

ItemKeyedDataSource Example

Following class is an example of ItemKeyedDataSource Which can be used instead of PageKeyedDataSource used in the above example.

public class CouponsItemKeyDataSource extends ItemKeyedDataSource<Integer, Coupon> {
    private CouponLocalDAO couponDAO;

    public CouponsItemKeyDataSource(Context ctx){
        couponDAO = LocalRepository.getCouponDB(ctx).couponDAO();
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);

        callback.onResult(cpns, 0, cpns.size());
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize( params.key, params.requestedLoadSize);
        callback.onResult(cpns);
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Coupon> callback) {

    }

    @NonNull
    @Override
    public Integer getKey(@NonNull Coupon item) {
        return item.get_id();
    }
}

original source : https://youtu.be/M15PEeHXM64 

33분량

select list형태의 경우 아래와 같이 array를 만들어 사용한다.

image

.

.

preference activity 나 preference fragment에 사용할 화면 구성하기 

image
image
image

.

.

preference activity 나 preference fragment의 형태 이경우에는 preference activity 안에 preference fragment를 포함 하는 형태로 만들었는데 반드시 포함해야 하는 것은 아니다.

image

.

.

저장된 Preferences를 통해 저장된 SharedPreferences값을 불러오는 방법과 일반 SharedPreferences사용방법

image

.

.

SharedPreferences와 Preferences 차이

https://stackoverflow.com/a/23223316/3151712

.

.

.

.

https://youtu.be/M15PEeHXM64

30분 분량

.

.

.

.

https://youtu.be/SfeRakSWWbk

16분분량

.

.

.

.

SharedPreferences Preferences setOnPreferenceChangeListener onPreferenceChange alert alertdialog

https://stackoverflow.com/a/21857176/3151712

https://stackoverflow.com/a/11531922/3151712

dialogpreference

https://developer.android.com/reference/android/preference/DialogPreference?hl=en

extend 

https://stackoverflow.com/a/40058062/3151712

.

.

.

.

Preference EditTextPreference 속성 attributes 바꾸기

https://stackoverflow.com/a/55461028/3151712

wasent개발하면서 알게 된점

  • addListenerForSingleValueEvent 를 통해서 snapshot을 얻어올수도 있지만 최초로 addCildEventListener를 node에 연결할때 되돌아 오는 snapshot을 이용할수 있다. 단 addCildEventListener 의 경우 listener가 연결된 지점이하 child 들의 snapshot이 한번에 오는 것이 아니고 순차적으로 하나씩오게 된다. 
  • node의 key, value개념과 child개념을 혼동하면 안된다. 
  • children을 통해 child들을 통으로 가져오려는 경우 children의 parent node에 listener를 연결한다. 때때로 wrapper parent를 만들어야 하는경우도 있다.
  • getValue()사용시 collection형태로 data를 가져오려는 경우 GenericTypeIndicator를 이용한다. 
val type = object : GenericTypeIndicator<HashMap<String,String>>() {}

            val precios : HashMap<String,String>  = dataSnapshot.getValue(type!!)
            liveData.postValue(precios)

ref)kotlin https://stackoverflow.com/a/52684990/3151712

ref)official docs  https://firebase.google.com/docs/reference/android/com/google/firebase/database/GenericTypeIndicator

.

.

.

.

.

google firebase realtime database official docs
https://firebase.google.com/docs/database/android/start

To make your app data update in realtime, you should add a ValueEventListener to the reference you just created. (ChildEventListerner는 현 node아래 child를 관찰하는 것이고 ValueEventListener는 현재node 및 child를 관찰하는 것이다)

The onDataChange() method in this class is triggered once when the listener is attached and again every time the data changes, including the children.

All Firebase Realtime Database data is stored as JSON objects. 그러므로 json으로 표현되기 쉬운 형태로 database구성을 생각한다. 

jacob.body.head.eyes.pupil 이런식으로 저장된다고 생각하고 jacob body head eyes pupil은 key이름이며 이안에 다양한 타입의 data가 들어있다. 또 jacob boddy head eyes pupil 은 각각 node이다. 

t’s best to keep your data structure as flat as possible. 너무 많이 nested되지 않게 하며 각 node의 데이터를 가져온다는 이야기는 하부의 모든 데이터도 같이 가져온다는 이야기가 되기 때문에 염두에 둔다.

https://firebase.google.com/docs/database/android/read-and-write

각 node의 reference에서 setValue()를 통해 값을 쓰고 교환하고 지우기(null을 설정)를 할수 있다.

저장가능한 데이터 형태는 아래와 같다.

Pass types that correspond to the available JSON types as follows:

  • String
  • Long
  • Double
  • Boolean
  • Map<String, Object>
  • List<Object>

Pass a custom Java object, 

  • if the class that defines it has a default constructor that takes no arguments and has public getters for the properties to be assigned.

To read data at a path and listen for changes, use the  addValueEventListener()  or  addListenerForSingleValueEvent()  method to add a ValueEventListener to a DatabaseReference. (addValueEventListener 나 addListenerForSingleValueEvent 둘다 ValueEventListener를 사용하는 것은 같다.  다만addListenerForSingleValueEvent는 한번 데이터 snapshot을 가져오고 listener 스스로 삭제된다)

image

위 그림에서 ValueEventListener also defines the onCancelled() method that is called if the read is canceled. For example, a read can be canceled if the client doesn’t have permission to read from a Firebase database location.

updateChildren()을 이용 해당 node의 여러 child node의 데이터를 하나의 작업으로 수정가능하다. (update만 가능한게 아니라 create도 가능하다.)

image

Simultaneous updates made this way are atomic: either all updates succeed or all updates fail.

setValue() and updateChildren() 작업에는 아래와 같이 listener를 달아서 바로 결과를 확인할수 있다. (setValue()로 create, delete,update작업을 할수 있으므로 모든 작업에 listener를 아래와 같이 달아서 확인 가능) 

image

특정 node에 덧붙여진 listener를 removeEventListener() 를 통해 제거해야 하며 덧붙여진 갯수 만큼 제거 해야 하고 . child node에 있는 것이 자동으로 제거되는 건 아니므로 child node에 따로 접근 제거해야 한다.

https://firebase.google.com/docs/database/android/lists-of-data

collection 형태의 자료를 가지고 있는 node의 경우 node에 ChildEventListener를 붙이면 onChildAdded, onChildChanged, onChildRemoved 을 통해 돌아 오는 snapshot이 각각 하나의 element가 된다. ValueEventListener를 사용하는 경우는 snapshot이 collection전체를 가지고 있게 된다. 이때    for (postSnapshot in dataSnapshot.children) {}    이런 형태로 접근하게 된다.

image

데이터 정렬방법은 위와 같으며 단 한개만 설정할수 있다.

image

위그림은 먼저 정렬된 상태에서 ChildEventListener를 붙이는 모습을 보여준다.

image

위그림은 nested child를 경로 지정으로 정렬기준을 조정하는 방법

image

위 query method는 여러개를 사용할수 있다.

query 성능 개선을 위해서는 index를 이용할수 있다. ( https://firebase.google.com/docs/database/security/indexing-data )

.

.

.

.

original source : https://www.youtube.com/playlist?list=PLk7v1Z2rk4hg3cbALQuTQOTgpZ9e9T8bi

image

.

.

image

.

.

image

addListenerForSingleValueEvent 현재 상태에서 snapshot가져오는 함수

addValueEventListener 하나의 노드 실시간 감시 

addCildEventListener 자식노드까지 실시간 감시

.

.

image

.

.

image
image

.

.

image
image

.

.

image
image

.

.

image
image
image

.

.

firebase realtime database

에는 두가지조건을 동시에 검색하는것이 없다.

image

이런것을 불가능하고

또한 두테이블의 relation 도 만들수 없다.

original source : https://stackoverflow.com/a/58663143/3151712

There are two different lifecycles because the Fragment itself lives longer than the Fragment’s view.

There are a number of events that can cause the Fragment’s view to be destroyed, but currently keep the Fragment itself alive:

  1. Putting the Fragment on the back stack (i.e., when you navigate() to another Fragment)
  2. Calling detach() on a Fragment
  3. Calling setMaxLifecycle(Lifecycle.State.CREATED) on a Fragment

In these cases, the Fragment’s view is destroyed, moving the Fragment view lifecycle to DESTROYED. However, the lifecycle of Fragment itself is not destroyed – it generally stays as CREATED. This means that the Fragment can go through multiple cycles of onCreateView() -> onViewCreated() -> onDestroyView() while only going through onCreate() once.

Where this becomes a problem is when it comes to how LiveData works. When you observe a LiveData, LiveData automatically unregisters the observer when the lifecycle reaches DESTROYED. But if you use the Fragment’s lifecycle to observe in onCreateView(), etc., that registered observer will still exist after onDestroyView(), despite the view being destroyed. This means that the second time your Fragment goes through onCreateView(), you’ll actually create a second active Observer, with both running simultaneously. Then three Observers the next time and on and on.

By using the view LifecycleOwner in onCreateView()/onViewCreated(), you ensure that you’ll only have one active Observer running at a time and that Observers tied to previous view instances are correctly destroyed along with the view. Therefore, yes, you should always use getViewLifecycleOwner() as the LifecycleOwner when in onCreateView() or onViewCreated(), including when using Data Binding.

Of course, if you’re registering an Observer in onCreate(), then the view LifecycleOwner does not exist yet (it is created right before onCreateView()) and you don’t have the multiple registration problem, which is why the Lint check specifically does not apply to any registrations done at onCreate() time. In those cases, using the Fragment’s lifecycle itself is absolutely correct.

As per the Fragments: Past, Present, and Future talk, one future improvements for Fragments is going to be combining the two lifecycles together, always destroying the Fragment whenever the Fragment’s view is destroyed. This is not yet available in any shipped version of Fragments, alpha or otherwise.

https://stackoverflow.com/a/57630353/3151712

질문) ViewModelProviders is deprecated So what’s the alternative to create the ViewModel’s object ?

답) 

boardViewModel = ViewModelProvider(this).get(BoardViewModel::class.java)

답)

private val viewModel: EntityGridViewModel by viewModels()

위의 방법으로 사용했지만 constructor가 있는 ViewModel의 경우 위의 구문실행시기가 이른 시기에 해당해서 constructor에 전달될 parameter를 준비하는데 시간이 부족한 경우가 많다. 그래서 다른 방법을 사용하게 되었다. 

https://www.albertgao.xyz/2018/04/13/how-to-add-additional-parameters-to-viewmodel-via-kotlin/

binding.authViewModel = ViewModelProviders.of(
 this,
 viewModelFactory { MyViewModel("albert") }
).get(AuthViewModel::class.java)

참고자료) passing parameters viewmodel

https://stackoverflow.com/a/60130631/3151712

https://www.albertgao.xyz/2018/04/13/how-to-add-additional-parameters-to-viewmodel-via-kotlin/

.

.

.

https://developer.android.com/topic/libraries/architecture/viewmodel

ViewModel 전반적인 사용 방법

.

.

위와 같이 사용하려고 하였으나 자동완성이 되지 않았다. dependency 설정이 잘못되어있었기 때문이다. 

    // ViewModel
//    android docs ref) https://developer.android.com/jetpack/androidx/releases/lifecycle
   def lifecycle_version = "2.2.0"
   def arch_version = "2.1.0"
   implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
   // LiveData
   implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

//    ref) https://developer.android.com/kotlin/ktx#fragment
   implementation "androidx.fragment:fragment-ktx:1.2.5"

   //ViewModel
//    from codelab ref) https://codelabs.developers.google.com/codelabs/kotlin-android-training-view-model/index.html?index=..%2F..android-kotlin-fundamentals#4
   implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'

가 필요했다. 

https://developer.android.com/topic/libraries/architecture/viewmodel#sharing

https://developer.android.com/kotlin/ktx#fragment

implementation "androidx.fragment:fragment-ktx:1.2.5"

.

.

.

.

주의사항)

https://developer.android.com/topic/libraries/architecture/viewmodel

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

image

.

.

.

.

하나의 activity 아래에 여러개의 fragment가 있고 activity lifecycle에 맞춰서 share view model을 사용하는 방법 

기본적으로 아래 내용은 그대로 이나 약간 문법이 바뀌었다. 

변화된 내용은 https://developer.android.com/topic/libraries/architecture/viewmodel#sharing 에서 확인할수 있다. 변화된 코드는 아래와 같다. 먼저 바로 아래 것을 보고 밑에것을 보면 된다. 

class SharedViewModel : ViewModel() {
   val selected = MutableLiveData<Item>()

   fun select(item: Item) {
       selected.value = item
   }
}

class MasterFragment : Fragment() {

   private lateinit var itemSelector: Selector

   // Use the 'by activityViewModels()' Kotlin property delegate
   // from the fragment-ktx artifact
   private val model: SharedViewModel by activityViewModels()

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
       itemSelector.setOnClickListener { item ->
           // Update the UI
       }
   }
}

class DetailFragment : Fragment() {

   // Use the 'by activityViewModels()' Kotlin property delegate
   // from the fragment-ktx artifact
   private val model: SharedViewModel by activityViewModels()

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
       model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
           // Update the UI
       })
   }
}

.

https://medium.com/mindorks/how-to-communicate-between-fragments-and-activity-using-viewmodel-ca733233a51c

Here comes ViewModel to rescue us from handling a lot of scenario and implementing interface . We only need to create ViewModel class and create instance in fragment but using the activity scope so that it will be available for all the fragment of the activity including activity itself.

Create a ViewModel class

class SharedViewModel:ViewModel(){
   val inputNumber = MutableLiveData<Int>()
}

To emit or pass data from our input fragment create ViewModel in activity scope. To do this we have to pass the activity reference as argument of the ViewModelProvides.of() method. Noe just pass the data to ViewModel object like this

activity?.let {
   sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}

et_input.addTextChangedListener(object : TextWatcher {
   override fun afterTextChanged(p0: Editable?) {}

   override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}

   override fun onTextChanged(txt: CharSequence?, p1: Int, p2: Int, p3: Int) {
       txt?.let {
           var input = 0
           if (txt.toString().isNotEmpty()) {
               input = txt.toString().toInt()
           }

           sharedViewModel?.inputNumber?.postValue(input)
       }
   }

In the activity we just need to create instance of our ViewModel and observe the required data like this

val sharedViewModel = ViewModelProviders.of(this).get(SharedViewModel::class.java)

sharedViewModel.inputNumber.observe(this, Observer {
   it?.let {
       // do some thing with the number
   }
})

Now what about output fragment? We can do same for the output fragment to observe the data. But keep in mind that we need create the ViewModel instance in activity scope, otherwise android will create a separate instance rather than sharing the same instance and we will not get the data.

For output fragment do it like this

activity?.let {
   val sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)

   sharedViewModel.inputNumber.observe(this, Observer {
   it?.let {
           // do some thing with the number
       }
   })
}

That’s it. Source code can be found here.

.

source code

class InputFragment : Fragment() {

    private var sharedViewModel: SharedViewModel? = null

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        activity?.let {
            /**
             *  create view model in activity scope
             */
            sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
        }

        et_input.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) {}

            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}

            override fun onTextChanged(txt: CharSequence?, p1: Int, p2: Int, p3: Int) {
                txt?.let {
                    var input = 0
                    if (txt.toString().isNotEmpty()) {
                        input = txt.toString().toInt()
                    }

                    sharedViewModel?.inputNumber?.postValue(input)
                }
            }
        })
    }

}
package ninja.shuza.sharedviewmodel

import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.properties.Delegates

class MainActivity : AppCompatActivity() {

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

        supportFragmentManager.beginTransaction().add(R.id.layout_top, InputFragment()).commit()
        supportFragmentManager.beginTransaction().add(R.id.layout_bottom, OutputFragment()).commit()

        val message = resources.getString(R.string.show_input)

        val sharedViewModel = ViewModelProviders.of(this).get(SharedViewModel::class.java)

        sharedViewModel.inputNumber.observe(this, Observer {
            it?.let {
                tv_show_input.text = "$message  $it"
            }
        })
    }
}
package ninja.shuza.sharedviewmodel

import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_output.*

/**
 *
 * :=  created by:  Shuza
 * :=  create date:  09-Aug-2018
 * :=  (C) CopyRight Shuza
 * :=  www.shuza.ninja
 * :=  shuza.sa@gmail.com
 * :=  Fun  :  Coffee  :  Code
 *
 **/
class OutputFragment : Fragment() {

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        /**
         *  create view model in activity scope
         */
        activity?.let {
            val sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)

            observeInput(sharedViewModel)
        }
    }

    private fun observeInput(sharedViewModel: SharedViewModel) {
        sharedViewModel.inputNumber.observe(this, Observer {
            it?.let {
                tv_output.text = "2 x $it  =  ${2 * it}"
            }
        })
    }

}

.

.

.

.

share viewmodel with several fragment

original source : https://medium.com/mindorks/how-to-communicate-between-fragments-and-activity-using-viewmodel-ca733233a51c

image

Here comes ViewModel to rescue us from handling a lot of scenario and implementing interface . We only need to create ViewModel class and create instance in fragment but using the activity scope so that it will be available for all the fragment of the activity including activity itself.

Create a ViewModel class

class SharedViewModel:ViewModel(){
   val inputNumber = MutableLiveData<Int>()
}

To emit or pass data from our input fragment create ViewModel in activity scope. To do this we have to pass the activity reference as argument of the ViewModelProvides.of() method. Noe just pass the data to ViewModel object like this

activity?.let {
   sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}

et_input.addTextChangedListener(object : TextWatcher {
   override fun afterTextChanged(p0: Editable?) {}

   override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}

   override fun onTextChanged(txt: CharSequence?, p1: Int, p2: Int, p3: Int) {
       txt?.let {
           var input = 0
           if (txt.toString().isNotEmpty()) {
               input = txt.toString().toInt()
           }

           sharedViewModel?.inputNumber?.postValue(input)
       }
   }

In the activity we just need to create instance of our ViewModel and observe the required data like this

val sharedViewModel = ViewModelProviders.of(this).get(SharedViewModel::class.java)

sharedViewModel.inputNumber.observe(this, Observer {
   it?.let {
       // do some thing with the number
   }
})

Now what about output fragment? We can do same for the output fragment to observe the data. But keep in mind that we need create the ViewModel instance in activity scope, otherwise android will create a separate instance rather than sharing the same instance and we will not get the data.

For output fragment do it like this

activity?.let {
   val sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)

   sharedViewModel.inputNumber.observe(this, Observer {
   it?.let {
           // do some thing with the number
       }
   })
}

That’s it. Source code can be found here.