In this tutorial, we’ll be digging deep into the concept of ViewModel. We’ll be developing a Favourite Links Application in Android wherein we can save our favorite links onto the SQLite database. We’ll see how ViewModel helps us in building a robust app.
在本教程中,我们将深入研究ViewModel的概念。 我们将在Android中开发一个“收藏夹链接”应用程序,其中可以将我们喜欢的链接保存到SQLite数据库中。 我们将看到ViewModel如何帮助我们构建强大的应用程序。
Android ViewModel (Android ViewModel)
ViewModel is an Android architecture component. It is used as a data manager in the application.
ViewModel是一个Android体系结构组件。 它在应用程序中用作数据管理器。
Letting the Activity do the data handling isn’t a good idea. A ViewModel provides a way to create and retrieve objects. It typically stores the state of a view’s data and communicates with other components.
让Activity进行数据处理不是一个好主意。 ViewModel提供了一种创建和检索对象的方法。 它通常存储视图数据的状态并与其他组件通信。
It is handy when it comes to configuration changes and the Activity’s data gets erased. The configuration change doesn’t impact the ViewModel since it is not tied to the Activity completely. It is able to provide the data to the Activity again after it gets recreated.
当涉及到配置更改并且活动的数据被删除时,它很方便。 配置更改不会影响ViewModel,因为它没有完全绑定到Activity。 重新创建活动后,它可以再次将数据提供给活动。
Android SQLite (Android SQLite)
We’ve discussed SQLite at depth in this tutorial. In that tutorial, we did all the SQLite handling in our Activity and made it a heavy class.
在本教程中,我们已经深入讨论了SQLite。 在该教程中,我们在Activity中完成了所有SQLite处理,并使其成为一个繁重的类。
Here we’ll do the SQLite querying in our ViewModel class.
在这里,我们将在ViewModel类中执行SQLite查询。
To recap: For adding data to SQLite database, we use a ContentValue which is like a data storage. We pass the data in the form of a key-value pair.
回顾一下:为了将数据添加到SQLite数据库,我们使用ContentValue,就像数据存储一样。 我们以键值对的形式传递数据。
To retrieve the data from the SQLite a Cursor object is used.
为了从SQLite检索数据,使用了一个Cursor对象。
In the following section, we’ll create an application that holds ListView records in an SQLite table.
在下一节中,我们将创建一个在SQLite表中保存ListView记录的应用程序。
You can add/delete your favorite web links to the ListView which would get updated in the database from the ViewModel
.
您可以将喜爱的Web链接添加/删除到ListView ,该链接将从ViewModel
在数据库中更新。
Android ViewModel教程项目结构 (Android ViewModel Tutorial Project Structure)
Add the following libraries in your app’s build.gradle
file:
在应用程序的build.gradle
文件中添加以下库:
implementation 'com.android.support:design:27.1.1'
implementation "android.arch.lifecycle:extensions:1.1.1"
Android ViewModel代码 (Android ViewModel Code)
The DB folder consists of the SQLite settings. The code for the DbSettings.java
class is given below:
DB文件夹包含SQLite设置。 下面给出了DbSettings.java
类的代码:
package com.journaldev.androidviewmodel.db;
import android.provider.BaseColumns;
public class DbSettings {
public static final String DB_NAME = "favourites.db";
public static final int DB_VERSION = 1;
public class DBEntry implements BaseColumns {
public static final String TABLE = "fav";
public static final String COL_FAV_URL = "url";
public static final String COL_FAV_DATE = "date";
}
}
The code for the FavouritesDbHelper.java
class is given below:
下面给出了FavouritesDbHelper.java
类的代码:
package com.journaldev.androidviewmodel.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class FavouritesDBHelper extends SQLiteOpenHelper {
public FavouritesDBHelper(Context context) {
super(context, DbSettings.DB_NAME, null, DbSettings.DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String createTable = "CREATE TABLE " + DbSettings.DBEntry.TABLE + " ( " +
DbSettings.DBEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
DbSettings.DBEntry.COL_FAV_URL + " TEXT NOT NULL, " +
DbSettings.DBEntry.COL_FAV_DATE + " INTEGER NOT NULL);";
db.execSQL(createTable);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DbSettings.DBEntry.TABLE);
onCreate(db);
}
}
Our database consists of three columns – ID, URL, DATE.
我们的数据库由三列组成-ID,URL,DATE。
The code for Favourites.java
class is given below:
下面给出了Favourites.java
类的代码:
package com.journaldev.androidviewmodel;
public class Favourites {
public long mId;
public String mUrl;
public long mDate;
public Favourites(long id, String name, long date) {
mId = id;
mUrl = name;
mDate = date;
}
public Favourites(Favourites favourites) {
mId = favourites.mId;
mUrl = favourites.mUrl;
mDate = favourites.mDate;
}
}
The code for the FavouritesViewModel.java class is given below:
下面给出了FavouritesViewModel.java类的代码:
package com.journaldev.androidviewmodel;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.journaldev.androidviewmodel.db.DbSettings;
import com.journaldev.androidviewmodel.db.FavouritesDBHelper;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class FavouritesViewModel extends AndroidViewModel {
private FavouritesDBHelper mFavHelper;
private ArrayList<Favourites> mFavs;
FavouritesViewModel(Application application) {
super(application);
mFavHelper = new FavouritesDBHelper(application);
}
public List<Favourites> getFavs() {
if (mFavs == null) {
mFavs = new ArrayList<>();
createDummyList();
loadFavs();
}
ArrayList<Favourites> clonedFavs = new ArrayList<>(mFavs.size());
for (int i = 0; i < mFavs.size(); i++) {
clonedFavs.add(new Favourites(mFavs.get(i)));
}
return clonedFavs;
}
public void createDummyList() {
addFav("https://www.journaldev.com", (new Date()).getTime());
addFav("https://www.medium.com", (new Date()).getTime());
addFav("https://www.reddit.com", (new Date()).getTime());
addFav("https://www.github.com", (new Date()).getTime());
addFav("https://www.hackerrank.com", (new Date()).getTime());
addFav("https://www.developers.android.com", (new Date()).getTime());
}
private void loadFavs() {
mFavs.clear();
SQLiteDatabase db = mFavHelper.getReadableDatabase();
Cursor cursor = db.query(DbSettings.DBEntry.TABLE,
new String[]{
DbSettings.DBEntry._ID,
DbSettings.DBEntry.COL_FAV_URL,
DbSettings.DBEntry.COL_FAV_DATE
},
null, null, null, null, null);
while (cursor.moveToNext()) {
int idxId = cursor.getColumnIndex(DbSettings.DBEntry._ID);
int idxUrl = cursor.getColumnIndex(DbSettings.DBEntry.COL_FAV_URL);
int idxDate = cursor.getColumnIndex(DbSettings.DBEntry.COL_FAV_DATE);
mFavs.add(new Favourites(cursor.getLong(idxId), cursor.getString(idxUrl), cursor.getLong(idxDate)));
}
cursor.close();
db.close();
}
public Favourites addFav(String url, long date) {
Log.d("API123", "addFav " + url);
SQLiteDatabase db = mFavHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(DbSettings.DBEntry.COL_FAV_URL, url);
values.put(DbSettings.DBEntry.COL_FAV_DATE, date);
long id = db.insertWithOnConflict(DbSettings.DBEntry.TABLE,
null,
values,
SQLiteDatabase.CONFLICT_REPLACE);
db.close();
Favourites fav = new Favourites(id, url, date);
mFavs.add(fav);
return new Favourites(fav);
}
public void removeFav(long id) {
SQLiteDatabase db = mFavHelper.getWritableDatabase();
db.delete(
DbSettings.DBEntry.TABLE,
DbSettings.DBEntry._ID + " = ?",
new String[]{Long.toString(id)}
);
db.close();
int index = -1;
for (int i = 0; i < mFavs.size(); i++) {
Favourites favourites = mFavs.get(i);
if (favourites.mId == id) {
index = i;
}
}
if (index != -1) {
mFavs.remove(index);
}
}
}
In the above code, we add, remove and load the data from the SQLite Database.
在上面的代码中,我们从SQLite数据库中添加,删除和加载数据。
In the getFavs()
method we do a deep clone of the ArrayList.
在getFavs()
方法中,我们对ArrayList进行了深层克隆。
The ViewModel is the data manager for our View. It supplies the data on demand.
ViewModel是我们View的数据管理器。 它按需提供数据。
The addFav() and removeFav() methods would get called from our Activity class for two operations:
从我们的Activity类中调用addFav()和removeFav()方法进行两项操作:
- Change the SQLite DB 更改SQLite数据库
- Change the data supplied to the ListView through ArrayList. 更改通过ArrayList提供给ListView的数据。
Thus every time the view requires something, our ViewModel has to do two calls. We'll see in a later tutorial, how using LiveData optimizes this.
因此,每次视图需要某些内容时,我们的ViewModel必须执行两次调用。 我们将在以后的教程中看到如何使用LiveData对此进行优化。
The code for the activity_main.xml layout is given below:
下面给出了activity_main.xml布局的代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:id="@+id/rlLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:src="@android:drawable/ic_input_add"
android:layout_margin="16dp" />
</RelativeLayout>
The code for the list_item_row.xml is given below:
list_item_row.xml的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/tvUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:padding="8dp"
android:textColor="@android:color/black"
android:textSize="20sp" />
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:onClick="deleteFav"
android:src="@android:drawable/ic_menu_delete" />
</RelativeLayout>
The code for the MainActivity.java class is given below:
MainActivity.java类的代码如下:
package com.journaldev.androidviewmodel;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import java.util.Date;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private FavouritesAdapter mFavAdapter;
private FavouritesViewModel mFavViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = findViewById(R.id.listView);
FloatingActionButton fab = findViewById(R.id.fab);
mFavViewModel = ViewModelProviders.of(this).get(FavouritesViewModel.class);
List<Favourites> favourites = mFavViewModel.getFavs();
mFavAdapter = new FavouritesAdapter(this, R.layout.list_item_row, favourites);
listView.setAdapter(mFavAdapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final EditText inUrl = new EditText(MainActivity.this);
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("New favourite")
.setMessage("Add a url link below")
.setView(inUrl)
.setPositiveButton("Add", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String url = String.valueOf(inUrl.getText());
long date = (new Date()).getTime();
// VM AND VIEW
mFavAdapter.add(mFavViewModel.addFav(url, date));
}
})
.setNegativeButton("Cancel", null)
.create();
dialog.show();
}
});
}
public void deleteFav(View view) {
View parent = (View) view.getParent();
int position = (int) parent.getTag(R.id.POS);
Favourites favourites = mFavAdapter.getItem(position);
mFavViewModel.removeFav(favourites.mId);
mFavAdapter.remove(favourites);
}
public class FavouritesAdapter extends ArrayAdapter<Favourites> {
private class ViewHolder {
TextView tvUrl;
TextView tvDate;
}
public FavouritesAdapter(Context context, int layoutResourceId, List<Favourites> todos) {
super(context, layoutResourceId, todos);
}
@Override
@NonNull
public View getView(int position, View convertView, ViewGroup parent) {
Favourites favourites = getItem(position);
ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = vi.inflate(R.layout.list_item_row, parent, false);
viewHolder.tvUrl = convertView.findViewById(R.id.tvUrl);
viewHolder.tvDate = convertView.findViewById(R.id.tvDate);
convertView.setTag(R.id.VH, viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag(R.id.VH);
}
viewHolder.tvUrl.setText(favourites.mUrl);
viewHolder.tvDate.setText((new Date(favourites.mDate).toString()));
convertView.setTag(R.id.POS, position);
return convertView;
}
}
}
In the above code, we've used an ArrayAdapter class for our ListView.
在上面的代码中,我们为ListView使用了ArrayAdapter类。
In our FloatingActionButton, we create an AlertDialog with an EditText to add a new URL in our ListView.
在FloatingActionButton中 ,我们创建一个带有EditText的AlertDialog ,以在ListView中添加新的URL。
ViewModel adds it to the SQLite and ArrayList.
ViewModel将其添加到SQLite和ArrayList。
There's an ImageButton defined for each list row in the XML. The android:onClick
method over there, invokes the method deleteFav()
.
在XML中为每个列表行定义了一个ImageButton。 那里的android:onClick
方法调用方法deleteFav()
。
In the deleteFav
we trigger two calls - One to the ViewModel to delete the record from SQLite and the other to the ArrayList. This is expensive. We'll see in a later tutorial how LiveData optimizes this.
在deleteFav
我们触发两个调用-一个调用ViewModel从SQLite删除记录,另一个调用ArrayList。 这很贵。 我们将在以后的教程中看到LiveData如何对此进行优化。
Clicking the tvUrl
would launch the URL directly into the browser since we'd set the android:autoLink="true"
单击tvUrl
会将URL直接启动到浏览器中,因为我们将android:autoLink="true"
Adapters that extend the ArrayAdapter class must pass the layout resource through the constructors. Else it can lead to duplicate rows in the ListView.
扩展ArrayAdapter类的适配器必须通过构造函数传递布局资源。 否则,可能导致ListView中的行重复。
We've assigned id tags to the View Holder and views in the ListView Adapter.
我们已将ID标记分配给View Holder和ListView Adapter中的视图。
The tag ids are defined in a file tags.xml in the values folder:
标签id在values文件夹中的文件tags.xml中定义:
The output of the above application in action is given below:
This brings an end to this tutorial. ViewModel is a vital of part of Android MVVM. You can download the final AndroidViewModel project from the link below:
本教程到此结束。 ViewModel是Android MVVM的重要组成部分。 您可以从下面的链接下载最终的AndroidViewModel项目: