1.Fragment
You can embed UI fragments in activities.
1. Debris communication
The fragment manager provides a finViewById, which is used to obtain instances of fragments from layout files
2. Life cycle of debris
1. Status
(1) Operating status: visible
(2) Pause status: covered but partially visible
(3) Stop status: completely invisible
(4) Destruction status: removed
2. Callback
(1)onAttach(). Debris is associated with activity.
(2)onCreateView(). Called when the fragment creates a view.
(3)onActivityCreated(). Ensure that the activity associated with the fragment must be called when it has been created.
(4)onDestory(). Called when the view associated with the fragment is removed.
(5)onDetach(). Called when the fragment is disassociated from the activity.
3. Dynamic loading layout
Use the qualifier to match the corresponding tablet and mobile phone.
4. System notification
Click to view the codeNote: for higher versions of Android, you need to pass in the channel and set the corresponding channeld to pop up the corresponding system notification. android:requestLegacyExternalStorage="true" this function needs to be set during external storage operationspackage com.example.notificationtest; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NotificationCompat; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends AppCompatActivity { final String CHANNEL_ID = "channel_id_1"; final String CHANNEL_NAME = "channel_name_1"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button sendNotice = (Button) findViewById(R.id.send_notice); sendNotice.setOnClickListener(new View.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onClick(View v) { Context context = getBaseContext(); Intent intent = new Intent(MainActivity.this,MainActivity.class); PendingIntent pi = PendingIntent.getActivity(MainActivity.this,0,intent,0); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); //Notification notification = new Notification(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_0_1) { NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID,CHANNEL_NAME,NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(notificationChannel); } Notification notification; notification = new NotificationCompat.Builder(MainActivity.this,CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setPriority(NotificationCompat.PRIORITY_MAX) .setContentTitle("Notice title") .setAutoCancel(true) .setContentText("This is a notification This is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notificationThis is a notification") .setContentIntent(pi) .setVibrate(new long[]{0,1000,1000,1000}) .setWhen(System.currentTimeMillis()) .build(); manager.notify(0,notification); } }); } } <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.notificationtest"> <uses-permission android:name="android.permission.VIBRATE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NotificationTest"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2.Service
1. Service life cycle
Lifecycle callback method:
(1)onCreate()
(2)onStartCommand()
(3)onBind()
(4)onDestory
After calling the startService() method, the corresponding service will start and call back the onStartCommand() method. If the service is not created, onCreate() will execute before the onStartCommand() method. Call stopService and unBindService methods to execute the onDestory method.
2. Front and rear desk service
Click to view the codepackage com.example.servicetest; import android.app.IntentService; import android.content.Intent; import android.util.Log; import androidx.annotation.Nullable; public class MyIntentService extends IntentService { /** * Creates an IntentService. Invoked by your subclass's constructor. * * @param name Used to name the worker thread, important only for debugging. */ public MyIntentService(String name) { super(name); } public MyIntentService(){ super(null); } @Override protected void onHandleIntent(@Nullable Intent intent) { Log.d("MyIntentService", "Thread id is" + Thread.currentThread().getId()); } @Override public void onDestroy() { super.onDestroy(); Log.d("MyIntentService", "onDestroy executed"); } } package com.example.servicetest; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.util.Log; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; public class MyService extends Service { public MyService() { } private DownloadBinder mBinder = new DownloadBinder(); class DownloadBinder extends Binder { public void startDownload() { Log.d("MyService", "startDownload executed"); } public int getProgress() { Log.d("MyService", "getProgress executed"); return 0; } } @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onCreate() { super.onCreate(); Log.d("MyService","onCreate executed"); Intent intent = new Intent(MyService.this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this,0, intent, 0); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationChannel notificationChannel = new NotificationChannel("channel_id","channel_name",NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(notificationChannel); Notification notification = new NotificationCompat.Builder(MyService.this,"channel_id") .setContentTitle("This is content title") .setContentText("This is content text") .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)) .setContentIntent(pi) .build(); startForeground(1, notification); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d("MyService","onStartCommand executed"); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d("MyService","onDestroy executed"); } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. return mBinder; } } package com.example.servicetest; import androidx.appcompat.app.AppCompatActivity; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.util.Log; import android.view.View; import android.widget.Button; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private MyService.DownloadBinder mDownloadBinder; private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mDownloadBinder = (MyService.DownloadBinder) service; mDownloadBinder.startDownload();; mDownloadBinder.getProgress(); } @Override public void onServiceDisconnected(ComponentName name) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startService = (Button) findViewById(R.id.startService); Button stopService = (Button) findViewById(R.id.stopService); Button bindService = (Button) findViewById(R.id.bindService); Button unbindService = (Button) findViewById(R.id.unbindService); Button startIntentService = (Button) findViewById(R.id.startIntentService); startService.setOnClickListener(this); stopService.setOnClickListener(this); bindService.setOnClickListener(this); unbindService.setOnClickListener(this); startIntentService.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.startService: Intent startIntent = new Intent(this,MyService.class); startService(startIntent); break; case R.id.stopService: Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent); break; case R.id.bindService: Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, mConnection, BIND_AUTO_CREATE); break; case R.id.unbindService: unbindService(mConnection); break; case R.id.startIntentService: Log.d("MainActivity","Thread id is" + Thread.currentThread().getId()); Intent intentService = new Intent(this, MyIntentService.class); startService(intentService); break; default: break; } } } <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.servicetest"> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ServiceTest"> <service android:name=".MyService" android:enabled="true" android:exported="true"></service> <service android:name=".MyIntentService" /> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
3. Multithreading
1. Asynchronous message processing mechanism
Asynchronous Message processing: it consists of four parts: Message, Hanlder, MessageQueue and Looper.
(1) Message: passing messages between threads, used to exchange data between different threads.
(2) Handler: used to send and process messages. sendMessgae() is passed to handlemessae after a series of processing.
(3) MessageQueue: used to store all messages sent through the Handler. Each thread will only have one MessageQueue object.
(4) Looper: when the MessageQueue in each thread is shut down, after calling the loop () method in looper, it will enter an infinite loop. If a message is found in the MessageQueue, it will be taken out and delivered to the HanleMessage method. There will also be only one looper object per thread.
2.AsyncTask usage
The corresponding task scheduling can be completed by re using the method
(1) onProExecute (): before the background task starts executing, it is called for initialization operation on the interface.
(2) doInBackground(Params...): all code in the method will be executed in the sub thread, and UI operations are not allowed
(3) onProgressUpdate(Progress...): this method will be called after the publishProgress(Progress...) method is called in the background. The returned data is passed as a parameter for Ui operation.
(4) onPostExecute(Result): after the background task is executed and returns, this method is called. The returned data is passed to this method as a parameter. You can use the returned data for UI operations.
4. Access the third-party SDK to implement and ServiceTest
Click to view the codepackage com.example.servicebestpratice; public interface DownloadListener { void onProgress(int progress); void onSuccess(); void onFailed(); void onPaused(); void onCancled(); } package com.example.servicebestpratice; import java.io.File; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.IBinder; import android.widget.Toast; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; public class DownloadService extends Service { private DownloadTask mDownloadTask; private String downloadUrl; private NotificationManager mManager; private DownloadListener mListener = new DownloadListener() { @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onProgress(int progress) { getNotificationManager().notify(1,getNotification("Downloading...",progress)); } @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onSuccess() { mDownloadTask = null; stopForeground(true); getNotificationManager().notify(1,getNotification("Download Success", -1)); Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show(); } @Override public void onFailed() { mDownloadTask = null; Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show(); } @Override public void onPaused() { mDownloadTask = null; Toast.makeText(DownloadService.this, "Download Paused", Toast.LENGTH_SHORT).show(); } @Override public void onCancled() { mDownloadTask = null; Toast.makeText(DownloadService.this, "Download Cancled", Toast.LENGTH_SHORT).show(); } }; public DownloadService() { } private DownloadBinder mBinder = new DownloadBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } class DownloadBinder extends Binder { @RequiresApi(api = Build.VERSION_CODES.O) public void startDownload(String url) { if (mDownloadTask == null) { downloadUrl = url; mDownloadTask = new DownloadTask(mListener); mDownloadTask.execute(downloadUrl); mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationChannel notificationChannel = new NotificationChannel("channelId","channelName",NotificationManager.IMPORTANCE_HIGH); mManager.createNotificationChannel(notificationChannel); startForeground(1,getNotification("Download...",0)); Toast.makeText(DownloadService.this, "Downloading...",Toast.LENGTH_SHORT).show(); } } public void pauseDownload() { if (mDownloadTask != null) { mDownloadTask.pauseDownload(); } } @RequiresApi(api = Build.VERSION_CODES.O) public void cancelDownload() { if (mDownloadTask != null) { mDownloadTask.cancelDownload(); } else { if (downloadUrl != null) { String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); File file = new File(directory + fileName); if (file.exists()) { file.delete(); } getNotificationManager().cancel(1); stopForeground(true); Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show(); } } } } @RequiresApi(api = Build.VERSION_CODES.O) private NotificationManager getNotificationManager() { return mManager; } private Notification getNotification(String title, int progress) { Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this,0,intent,0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"channelId"); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)); builder.setContentIntent(pi); builder.setContentTitle(title); if (progress > 0) { builder.setContentText(progress + "%"); builder.setProgress(100,progress,false); } return builder.build(); } } package com.example.servicebestpratice; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import android.os.AsyncTask; import android.os.Environment; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class DownloadTask extends AsyncTask<String, Integer, Integer> { public static final int TYPE_SUCCESS = 0; public static final int TYPE_FAILED = 1; public static final int TYPE_PAUSED = 2; public static final int TYPE_CANCELED = 3; private DownloadListener mListener; private boolean isCanceled = false; private boolean isPaused = false; private int lastProgress; public DownloadTask(DownloadListener listener) { this.mListener = listener; } @Override protected Integer doInBackground(String... strings) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try { long downloadedLength = 0; String downloadUrl = strings[0]; String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); file = new File(directory + fileName); if (file.exists()) { downloadedLength = file.length(); } long contentLength = getContentLength(downloadUrl); if (contentLength == 0) { return TYPE_FAILED; } else if (contentLength == downloadedLength) { return TYPE_SUCCESS; } OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .addHeader("RANGE","bytes=" + downloadedLength + "-") .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if (response != null) { is = response.body().byteStream(); savedFile = new RandomAccessFile(file, "rw"); savedFile.seek(downloadedLength); byte[] b = new byte[1024]; int total = 0; int len; while ((len = is.read(b))!=-1) { if (isCanceled) { return TYPE_CANCELED; } else if (isPaused) { return TYPE_PAUSED; } else { total += len; savedFile.write(b,0,len); int progress = (int)((total+downloadedLength) * 100 / contentLength); publishProgress(progress); } } response.close(); return TYPE_SUCCESS; } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (savedFile != null) { savedFile.close(); } if (isCanceled && file != null) { file.delete(); } } catch (IOException e) { e.printStackTrace(); } } return null; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); int progress = values[0]; if (progress > lastProgress) { mListener.onProgress(progress); lastProgress = progress; } } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); switch (integer) { case TYPE_CANCELED: mListener.onCancled(); break; case TYPE_SUCCESS: mListener.onSuccess(); break; case TYPE_PAUSED: mListener.onPaused(); break; case TYPE_FAILED: mListener.onFailed(); default: break; } } private long getContentLength(String downloadUrl) throws IOException { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if (response != null && response.isSuccessful()) { long contentLength = response.body().contentLength(); response.close(); return contentLength; } return 0; } public void pauseDownload() { isPaused = true; } public void cancelDownload() { isCanceled = true; } } package com.example.servicebestpratice; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.baidu.location.BDAbstractLocationListener; import com.baidu.location.BDLocation; import com.baidu.location.LocationClient; import com.baidu.location.LocationClientOption; import com.baidu.mapapi.map.BaiduMap; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private DownloadService.DownloadBinder mDownloadBinder; private LocationClient mLocationClient; private MyLocationListener mListener = new MyLocationListener(); private TextView positionText; private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mDownloadBinder = (DownloadService.DownloadBinder) service; } @Override public void onServiceDisconnected(ComponentName name) { } }; public class MyLocationListener extends BDAbstractLocationListener { @Override public void onReceiveLocation(BDLocation bdLocation) { double latitude = bdLocation.getLatitude(); double longitude = bdLocation.getLongitude(); float radius = bdLocation.getRadius(); String coorType = bdLocation.getCoorType(); int errorCode = bdLocation.getLocType(); positionText.setText(latitude + " ++ " +longitude + " ++ " +radius + " ++ " +coorType + " ++ "); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLocationClient = new LocationClient(getApplicationContext()); mLocationClient.registerLocationListener(mListener); LocationClientOption option = new LocationClientOption(); option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); option.setScanSpan(1000); mLocationClient.setLocOption(option); mLocationClient.start(); Button startDownload = (Button) findViewById(R.id.start_download); Button pauseDownload = (Button) findViewById(R.id.pause_download); Button cancelDownload = (Button) findViewById(R.id.cancel_download); positionText = (TextView) findViewById(R.id.position_text_view); startDownload.setOnClickListener(this); pauseDownload.setOnClickListener(this); cancelDownload.setOnClickListener(this); Intent intent = new Intent(this, DownloadService.class); startService(intent); bindService(intent, mConnection, BIND_AUTO_CREATE); if (ContextCompat .checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1); } } @SuppressLint("NonConstantResourceId") @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onClick(View v) { if (mDownloadBinder == null) { return; } switch (v.getId()) { case R.id.start_download: String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe"; mDownloadBinder.startDownload(url); break; case R.id.pause_download: mDownloadBinder.pauseDownload(); break; case R.id.cancel_download: mDownloadBinder.cancelDownload(); break; default: break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 1) { if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "failed", Toast.LENGTH_SHORT).show(); finish(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @Override protected void onDestroy() { super.onDestroy(); unbindService(mConnection); } } <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/start_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start Downlaod" /> <Button android:id="@+id/pause_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Pause Downlaod" /> <Button android:id="@+id/cancel_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Cancel Downlaod" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/position_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> plugins { id 'com.android.application' } android { compileSdk 31 defaultConfig { ndk { // Set the supported so Library Architecture (developers can select so of one or more platforms as needed) abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86","x86_64" } applicationId "com.example.servicebestpratice" minSdk 21 targetSdk 31 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', includes: ['*.jar']) implementation("com.squareup.okhttp3:okhttp:4.2.0") implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'com.baidu.lbsyun:BaiduMapSDK_Map:7.4.0' implementation 'com.baidu.lbsyun:BaiduMapSDK_Util:7.4.0' implementation 'com.baidu.lbsyun:BaiduMapSDK_Search:7.4.0' implementation 'com.baidu.lbsyun:BaiduMapSDK_Location:9.1.8' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' }