socialgekon.com
  • Основен
  • Back-End
  • Уеб Интерфейс
  • Рентабилност И Ефективност
  • Ui Design
Подвижен

Нишки за Android: Всичко, което трябва да знаете

Всеки разработчик на Android, в един или друг момент, трябва да се справя с нишки в своето приложение.

Когато приложението се стартира в Android, то създава първата нишка на изпълнение, известна като „основната“ нишка. Основната нишка е отговорна за изпращане на събития към подходящите приспособления на потребителския интерфейс, както и комуникация с компоненти от инструментариума на потребителския интерфейс на Android.

За да поддържате приложението си отзивчиво, от съществено значение е да избягвате използването на основната нишка за извършване на всяка операция, която може да доведе до блокиране.



Мрежовите операции и обажданията към база данни, както и зареждането на определени компоненти, са често срещани примери за операции, които човек трябва да избягва в основната нишка. Когато се извикат в основната нишка, те се извикват синхронно, което означава, че потребителският интерфейс ще остане напълно неотговарящ, докато операцията завърши. Поради тази причина те обикновено се изпълняват в отделни нишки, което по този начин избягва блокирането на потребителския интерфейс, докато се изпълнява (т.е. те се изпълняват асинхронно от потребителския интерфейс).

Android предоставя много начини за създаване и управление на нишки и съществуват много библиотеки на трети страни, които правят управлението на нишките много по-приятно. Въпреки това, с толкова много различни подходи под ръка, изборът на правилния може да бъде доста объркващ.

В тази статия ще научите за някои често срещани сценарии в Разработка на Android където резбата става от съществено значение и някои прости решения, които могат да бъдат приложени към тези сценарии и не само.

Конци в Android

В Android можете да категоризирате всички компоненти за резби в две основни категории:

  1. Конци, които са прикрепен към дейност / фрагмент: Тези нишки са обвързани с жизнения цикъл на активността / фрагмента и се прекратяват веднага щом активността / фрагментът бъде унищожен.
  2. Конци, които не са прикачен към всяка дейност / фрагмент: Тези нишки могат да продължат да се изпълняват отвъд живота на активността / фрагмента (ако има такъв), от който са породени.

Компоненти за нишки, които се прикачват към дейност / фрагмент

AsyncTask

AsyncTask е най-основният Android компонент за резба. Той е лесен за използване и може да бъде добър за основни сценарии.

Примерно използване:

public class ExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MyTask().execute(url); } private class MyTask extends AsyncTask { @Override protected String doInBackground(String... params) { String url = params[0]; return doSomeWork(url); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); // do something with result } } }

AsyncTask, обаче се проваля, ако имате нужда от вашата отложена задача да продължи след живота на активността / фрагмента. Струва си да се отбележи, че дори нещо толкова просто като въртенето на екрана може да доведе до унищожаване на активността.

Товарачи

Товарачите са решението на споменатия по-горе проблем. Зареждащите устройства могат автоматично да спрат, когато активността бъде унищожена, а също така могат да се рестартират след пресъздаването на дейността.

Има основно два вида товарачи: AsyncTaskLoader и CursorLoader. Ще научите повече за CursorLoader по-късно в тази статия.

AsyncTaskLoader е подобно на AsyncTask, но малко по-сложно.

Примерно използване:

public class ExampleActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(1, null, new MyLoaderCallbacks()); } private class MyLoaderCallbacks implements LoaderManager.LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new MyLoader(ExampleActivity.this); } @Override public void onLoadFinished(Loader loader, Object data) { } @Override public void onLoaderReset(Loader loader) { } } private class MyLoader extends AsyncTaskLoader { public MyLoader(Context context) { super(context); } @Override public Object loadInBackground() { return someWorkToDo(); } } }

Компоненти за нишки, които не се прикачват към дейност / фрагмент

Обслужване

Service е компонент, който е полезен за извършване на дълги (или потенциално дълги) операции без никакъв потребителски интерфейс.

Service работи в основната нишка на хостинг процеса; услугата не създава собствена нишка и не се изпълнява в отделен процес, освен ако не посочите друго.

Примерно използване:

public class ExampleService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { doSomeLongProccesingWork(); stopSelf(); return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }

С Service е така Вашият отговорността да го спрете, когато работата му приключи, като извикате или stopSelf() или stopService() метод.

IntentService

Като Service, IntentService работи на отделна нишка и се спира автоматично, след като завърши работата си.

IntentService обикновено се използва за кратки задачи, които не е необходимо да се прикачват към който и да е потребителски интерфейс.

Примерно използване:

public class ExampleService extends IntentService { public ExampleService() { super('ExampleService'); } @Override protected void onHandleIntent(Intent intent) { doSomeShortWork(); } }

Седем шаблона за резби в Android

Случай за употреба № 1: Изпращане на заявка през мрежа, без да се изисква отговор от сървъра

Понякога може да искате да изпратите заявка за API до сървър, без да се притеснявате за отговора му. Например може да изпращате маркер за push регистрация до задния край на приложението си.

Тъй като това включва отправяне на заявка по мрежата, трябва да го направите от нишка, различна от основната нишка.

Вариант 1: AsyncTask или товарачи

Можете да използвате AsyncTask или товарачи за осъществяване на обаждането и ще работи.

Въпреки това, AsyncTask и товарачите са зависими от жизнения цикъл на дейността. Това означава, че или ще трябва да изчакате повикването да се изпълни и да се опитате да попречите на потребителя да напусне активността, или да се надявате, че ще се изпълни, преди активността да бъде унищожена.

Вариант 2: Обслужване

Service може да е по-подходящ за този случай на употреба, тъй като не е свързан с никаква дейност. Следователно той ще може да продължи с мрежовото повикване дори след като активността бъде унищожена. Освен това, тъй като отговорът от сървъра не е необходим, и тук услугата не би била ограничаваща.

Тъй като обаче услугата ще започне да се изпълнява на нишката на потребителския интерфейс, пак ще трябва да управлявате себе си. Също така ще трябва да се уверите, че услугата е спряна, след като мрежовото повикване завърши.

Това би изисквало повече усилия, отколкото би било необходимо за такова просто действие.

Вариант 3: IntentService

Това според мен би било най-добрият вариант.

Тъй като IntentService не се привързва към никаква дейност и работи на нишка, която не е с потребителски интерфейс, тук перфектно обслужва нуждите ни. Освен това, IntentService спира се автоматично, така че няма нужда и ръчно да го управлявате.

Използвайте случай № 2: Осъществяване на мрежово повикване и получаване на отговор от сървъра

Този случай на употреба вероятно е малко по-често срещан. Например, може да искате да извикате API в задния край и да използвате отговора му за попълване на полета на екрана.

Вариант 1: Услуга или IntentService

Въпреки че Service или IntentService се справи добре за предишния случай на употреба, използването им тук не би било добра идея. Опит за извличане на данни от Service или IntentService в основната нишка на потребителския интерфейс би направило нещата много сложни.

Вариант 2: AsyncTask или товарачи

Първо руж, AsyncTask или товарачите изглежда са очевидното решение тук. Те са лесни за използване - прости и ясни.

Въпреки това, когато използвате AsyncTask или товарачи, ще забележите, че е необходимо да напишете някакъв код за шаблон. Освен това обработката на грешки се превръща в основна работа с тези компоненти. Дори и с обикновено обаждане в мрежа, трябва да сте наясно с потенциалните изключения, да ги хванете и да действате по съответния начин. Това ни принуждава да обгърнем отговора си в персонализиран клас, съдържащ данните, с възможна информация за грешка, а флаг показва дали действието е било успешно или не.

Това е доста работа за всяко едно обаждане. За щастие сега има много по-добро и опростено решение: RxJava.

Вариант 3: RxJava

Може да сте чували за RxJava , библиотеката, разработена от Netflix. Това е почти магия в Java.

RxAndroid ви позволява да използвате RxJava в Android и прави работата с асинхронни задачи бриз. Можете да научите повече за RxJava на Android тук .

RxJava предоставя два компонента: Observer и Subscriber.

An наблюдател е компонент, който съдържа някакво действие. Той изпълнява това действие и връща резултата, ако успее, или грешка, ако не успее.

ДА СЕ абонат , от друга страна, е компонент, който може да получи резултата (или грешката) от наблюдаем, като се абонира за него.

С RxJava първо създавате наблюдаемо:

Observable.create((ObservableOnSubscribe) e -> { Data data = mRestApi.getData(); e.onNext(data); })

След като наблюдаваното е създадено, можете да се абонирате за него.

С библиотеката RxAndroid можете да контролирате нишката, в която искате да изпълните действието в наблюдаемия, и нишката, в която искате да получите отговора (т.е. резултата или грешката).

Вие обвързвате наблюдаемото с тези две функции:

.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()

Планировъците са компоненти, които изпълняват действието в определена нишка. AndroidSchedulers.mainThread() е планировчикът, свързан с основната нишка.

Като се има предвид, че нашето API извикване е mRestApi.getData() и връща a Data обект, основното повикване може да изглежда така:

Observable.create((ObservableOnSubscribe) e -> { try { Data data = mRestApi.getData(); e.onNext(data); } catch (Exception ex) { e.onError(ex); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(match -> Log.i(“rest api, 'success'), throwable -> Log.e(“rest api, 'error: %s' + throwable.getMessage()));

Без дори да навлизаме в други предимства от използването на RxJava, вече можете да видите как RxJava ни позволява да напишем по-зрял код, като абстрахираме сложността на нишките.

Използвайте случай № 3: Свързване на мрежови разговори

За мрежови повиквания, които трябва да се извършват последователно (т.е. когато всяка операция зависи от отговора / резултата от предишната операция), трябва да бъдете особено внимателни при генерирането на код за спагети.

Например може да се наложи да осъществите API повикване с маркер, който трябва да изтеглите първо чрез друго API повикване.

Вариант 1: AsyncTask или товарачи

Използване на AsyncTask или товарачите почти определено ще доведат до код за спагети. Цялостната функционалност ще бъде трудно да се получи правилно и ще изисква огромно количество излишен код на шаблона по време на вашия проект.

Вариант 2: RxJava с помощта на flatMap

В RxJava, flatMap оператор взема излъчена стойност от източника на наблюдаемо и връща друго наблюдаемо. Можете да създадете наблюдаемо и след това да създадете друго наблюдаемо, като използвате излъчената стойност от първата, която основно ще ги веригира.

Етап 1. Създайте наблюдаемото, което извлича маркера:

public Observable getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }

Стъпка 2. Създайте наблюдаемото, което получава данните с помощта на маркера:

public Observable getDataObservable(String token) { return Observable.create(subscriber -> { try { Data data = mRestApi.getData(token); subscriber.onNext(data); } catch (IOException e) { subscriber.onError(e); } }); }

Стъпка 3. Свържете двете наблюдаеми заедно и се абонирайте:

getTokenObservable() .flatMap(new Function() { @Override public Observable apply(String token) throws Exception { return getDataObservable(token); } }) .subscribe(data -> { doSomethingWithData(data) }, error -> handleError(e));

Имайте предвид, че използването на този подход не се ограничава до мрежови разговори; той може да работи с всякакъв набор от действия, които трябва да се изпълняват в последователност, но на отделни нишки.

Всички случаи на употреба по-горе са доста прости. Превключването между нишките се случва само след като всеки завърши своята задача. По-напреднали сценарии - например, когато две или повече нишки трябва активно да комуникират помежду си - могат да бъдат подкрепени и от този подход.

Случай за употреба № 4: Комуникирайте с потребителския интерфейс от друга нишка

Помислете за сценарий, при който искате да качите файл и да актуализирате потребителския интерфейс, след като завърши.

Тъй като качването на файл може да отнеме много време, не е необходимо потребителят да чака. Можете да използвате услуга и вероятно IntentService за внедряване на функционалността тук.

В този случай обаче по-голямото предизвикателство е възможността да се извика метод в потребителския интерфейс след като качването на файла (което е извършено в отделна нишка) приключи.

Вариант 1: RxJava вътре в услугата

RxJava, самостоятелно или в IntentService, може да не е идеален. Ще трябва да използвате механизъм, базиран на обратно повикване, когато се абонирате за Observable и IntentService е създаден да прави прости синхронни обаждания, а не обратно извикване.

От друга страна, с Service ще трябва да спрете ръчно услугата, което изисква повече работа.

Вариант 2: BroadcastReceiver

Android предоставя този компонент, който може да слуша глобални събития (например събития от батерията, мрежови събития и т.н.), както и персонализирани събития. Можете да използвате този компонент, за да създадете персонализирано събитие, което се задейства, когато качването приключи.

За да направите това, трябва да създадете персонализиран клас, който се разширява BroadcastReceiver, да го регистрирате в манифеста и да използвате Intent и IntentFilter за да създадете персонализирано събитие. За да задействате събитието, ще ви трябва sendBroadcast метод.

Манифест:

public class UploadReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getBoolean(“success”, false) { Activity activity = (Activity)context; activity.updateUI(); } }

Приемник:

Intent intent = new Intent(); intent.setAction('com.example.upload'); sendBroadcast(intent);

Подател:

Handler

Този подход е жизнеспособен вариант. Но както забелязахте, това включва известна работа и твърде много предавания могат да забавят нещата.

Вариант 3: Използване на Handler

A Runnable е компонент, който може да се прикачи към нишка и след това да се извърши някакво действие върху тази нишка чрез прости съобщения или Looper задачи. Той работи заедно с друг компонент, Handler, който отговаря за обработката на съобщения в определена нишка.

Когато a Looper е създаден, той може да получи Looper.getMainLooper() обект в конструктора, който показва към коя нишка е прикрепен манипулаторът. Ако искате да използвате манипулатор, прикрепен към основната нишка, трябва да използвате цикъла, свързан с основната нишка, като извикате Runnable.

В този случай, за да актуализирате потребителския интерфейс от фонова нишка, можете да създадете манипулатор, прикрепен към нишката на потребителския интерфейс, и след това да публикувате действие като Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { // update the ui from here } }); :

EventBus

Този подход е много по-добър от първия, но има още по-опростен начин да направите това ...

Вариант 3: Използване на EventBus

UIEvent , популярна библиотека от GreenRobot, позволява на компонентите безопасно да комуникират помежду си. Тъй като случаят ни на употреба е такъв, при който искаме само да актуализираме потребителския интерфейс, това може да бъде най-простият и безопасен избор.

Етап 1. Създайте клас на събитие. напр. @Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {/* Do something */}; register and unregister eventbus : @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); } .

Стъпка 2. Абонирайте се за събитието.

EventBus.getDefault().post(new UIEvent());

Стъпка 3. Публикувайте събитието: ThreadMode

С UIEvent параметър в анотацията, вие посочвате нишката, за която искате да се абонирате за това събитие. В нашия пример тук избираме основната нишка, тъй като ще искаме получателят на събитието да може да актуализира потребителския интерфейс.

Можете да структурирате вашия class UploadFileService extends IntentService { // … Boolean success = uploadFile(File file); EventBus.getDefault().post(new UIEvent(success)); // ... } клас, за да съдържа допълнителна информация, ако е необходимо.

В услугата:

@Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {//show message according to the action success};

В активността / фрагмента:

EventBus library

Използвайки EventBus, комуникацията между нишките става много по-опростена.

Случай за употреба № 5: Двупосочна комуникация между нишки въз основа на действия на потребителя

Да предположим, че изграждате мултимедиен плейър и искате той да може да продължи да възпроизвежда музика дори когато екранът на приложението е затворен. В този сценарий ще искате потребителският интерфейс да може да комуникира с медийната нишка (например възпроизвеждане, пауза и други действия) и също така медийната нишка да актуализира потребителския интерфейс въз основа на определени събития (например грешка, състояние на буфериране и т.н.).

Пълен пример за медиен плейър е извън обхвата на тази статия. Можете обаче да намерите добри уроци тук и тук .

Вариант 1: Използване на EventBus

Можете да използвате BoundService тук. Обикновено обаче е опасно да се публикува събитие от потребителския интерфейс и да се получи в услуга. Това е така, защото нямате начин да разберете дали услугата работи, когато сте изпратили съобщението.

Вариант 2: Използване на BoundService

A Service е Binder което е свързано с дейност / фрагмент. Това означава, че активността / фрагментът винаги знае дали услугата се изпълнява или не и освен това получава достъп до публичните методи на услугата.

За да го приложите, трябва да създадете персонализиран public class MediaService extends Service { private final IBinder mBinder = new MediaBinder(); public class MediaBinder extends Binder { MediaService getService() { // Return this instance of LocalService so clients can call public methods return MediaService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } } вътре в услугата и създайте метод, който връща услугата.

ServiceConnection

За да обвържете активността с услугата, трябва да внедрите bindService, който е класът, наблюдаващ състоянието на услугата, и да използвате метода // in the activity MediaService mService; // flag indicates the bound status boolean mBound; @Override protected void onStart() { super.onStart(); // Bind to LocalService Intent intent = new Intent(this, MediaService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { MediaBinder binder = (MediaBinder) service; mService = binder.getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { mBound = false; } }; за да направите обвързването:

BroadcastReceiver

Можете да намерите пълен пример за изпълнение тук .

За да комуникирате с услугата, когато потребителят докосне бутона Възпроизвеждане или Пауза, можете да се свържете с услугата и след това да извикате съответния публичен метод за услугата.

Когато има медийно събитие и искате да го предадете обратно на активността / фрагмента, можете да използвате една от по-ранните техники (например Handler, EventBus или merge()).

Използвайте случай № 6: Паралелно изпълнение на действия и получаване на резултати

Да предположим, че създавате туристическо приложение и искате да покажете атракции на карта, извлечена от множество източници (различни доставчици на данни). Тъй като не всички източници може да са надеждни, може да искате да игнорирате неуспешните и да продължите да изобразявате картата така или иначе.

За да паралелизирате процеса, всяко API извикване трябва да се извършва в различна нишка.

Вариант 1: Използване на RxJava

В RxJava можете да комбинирате множество наблюдаеми в едно, използвайки concat() или ExecutorService оператори. След това можете да се абонирате за „обединеното“ наблюдаемо и да изчакате всички резултати.

Този подход обаче няма да работи според очакванията. Ако едно API повикване се провали, обединеният наблюдаем ще отчете общ неуспех.

Вариант 2: Използване на естествени Java компоненти

Future в Java създава фиксиран (конфигурируем) брой нишки и изпълнява едновременно задачи върху тях. Услугата връща a invokeAll() обект, който в крайна сметка връща всички резултати чрез ExecutorService метод.

Всяка задача, която изпращате на Callable трябва да се съдържа в invokeAll() интерфейс, който е интерфейс за създаване на задача, която може да създаде изключение.

След като получите резултатите от ExecutorService pool = Executors.newFixedThreadPool(3); List tasks = new ArrayList(); tasks.add(new Callable() { @Override public Integer call() throws Exception { return mRest.getAttractionType1(); } }); // ... try { List results = pool.invokeAll(tasks); for (Future result : results) { try { Object response = result.get(); if (response instance of AttractionType1... {} if (response instance of AttractionType2... {} ... } catch (ExecutionException e) { e.printStackTrace(); } } } catch (InterruptedException e) { e.printStackTrace(); } , можете да проверите всеки резултат и да продължите съответно.

Да кажем например, че имате три вида привличане, идващи от три различни крайни точки и искате да направите три паралелни обаждания:

Cursor

По този начин вие изпълнявате всички действия успоредно. Следователно можете да проверите за грешки във всяко действие поотделно и да игнорирате отделни грешки, както е подходящо.

Този подход е по-лесен от използването на RxJava. Той е по-прост, по-кратък и не проваля всички действия поради едно изключение.

Случай на употреба № 7: Заявка за локална база данни на SQLite

Когато се занимавате с локална база данни на SQLite, се препоръчва базата данни да се използва от фонова нишка, тъй като повикванията на базата данни (особено при големи бази данни или сложни заявки) могат да отнемат много време, което води до замразяване на потребителския интерфейс.

При заявка за данни на SQLite получавате Cursor cursor = getData(); String name = cursor.getString(); обект, който след това може да се използва за извличане на действителните данни.

public Observable getLocalDataObservable() { return Observable.create(subscriber -> { Cursor cursor = mDbHandler.getData(); subscriber.onNext(cursor); }); }

Вариант 1: Използване на RxJava

Можете да използвате RxJava и да получите данните от базата данни, точно както получаваме данни от нашия back-end:

getLocalDataObservable()

Можете да използвате наблюдаваното, върнато от getLocalDataObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(cursor -> String name = cursor.getString(0), throwable -> Log.e(“db, 'error: %s' + throwable.getMessage())); както следва:

CursorLoader

Въпреки че това със сигурност е добър подход, има още по-добър, тъй като има компонент, който е създаден точно за този сценарий.

Вариант 2: Използване на CursorLoader + ContentProvider

Android предоставя Loader, естествен компонент за зареждане на данни на SQLite и управление на съответната нишка. Това е Cursor което връща getString(), което можем да използваме, за да получим данните, като извикаме прости методи като getLong(), public class SimpleCursorLoader extends FragmentActivity implements LoaderManager.LoaderCallbacks { public static final String TAG = SimpleCursorLoader.class.getSimpleName(); private static final int LOADER_ID = 0x01; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_cursor_loader); textView = (TextView) findViewById(R.id.text_view); getSupportLoaderManager().initLoader(LOADER_ID, null, this); } public Loader onCreateLoader(int i, Bundle bundle) { return new CursorLoader(this, Uri.parse('content://com.github.browep.cursorloader.data') , new String[]{'col1'}, null, null, null); } public void onLoadFinished(Loader cursorLoader, Cursor cursor) { if (cursor != null && cursor.moveToFirst()) { String text = textView.getText().toString(); while (cursor.moveToNext()) { text += '
' + cursor.getString(1); cursor.moveToNext(); } textView.setText(Html.fromHtml(text) ); } } public void onLoaderReset(Loader cursorLoader) { } }
и т.н.

CursorLoader

ContentProvider работи с

|_+_|
съставна част. Този компонент предоставя множество функции в базата данни в реално време (напр. Известия за промени, задействания и т.н.), които позволяват на разработчиците да внедрят по-лесно потребителско изживяване много по-лесно.

В Android няма решение Silver Bullet за нишките

Android предоставя много начини за обработка и управление на нишки, но нито един от тях не е сребърни куршуми.

Изборът на правилния подход за резби, в зависимост от вашия случай на употреба, може да направи всичко различно в улесняването на цялостното решение за прилагане и разбиране. Родните компоненти се вписват добре за някои случаи, но не за всички. Същото важи и за изисканите решения на трети страни.

Надявам се тази статия да ви бъде полезна, когато работите по следващия си проект за Android. Споделете с нас своя опит с нишки в Android или всеки случай на употреба, при който горните решения работят добре - или не, в този смисъл - в коментарите по-долу.

Ръководство за Monorepos за Front-end Code

Подвижен

Ръководство за Monorepos за Front-end Code
Ръководител на публикации

Ръководител на публикации

Други

Популярни Публикации
ApeeScape стартира Staffing.com, за да насочи напред концерта, свободната практика и икономиката на талантите
ApeeScape стартира Staffing.com, за да насочи напред концерта, свободната практика и икономиката на талантите
Защо вече трябва да надстроите до Java 8
Защо вече трябва да надстроите до Java 8
Stars Realigned: Подобряване на системата за оценяване IMDb
Stars Realigned: Подобряване на системата за оценяване IMDb
Решения, а не изкуство - истинската бизнес стойност на дизайна
Решения, а не изкуство - истинската бизнес стойност на дизайна
Управление на отдалечени фрийлансъри? Тези принципи ще помогнат
Управление на отдалечени фрийлансъри? Тези принципи ще помогнат
 
Видео анализ на машинно обучение: Идентифициране на риби
Видео анализ на машинно обучение: Идентифициране на риби
Проучване на мечото дело на криптовалутния балон
Проучване на мечото дело на криптовалутния балон
Пълното ръководство за филтрите на камерата на iPhone (включително скритите)
Пълното ръководство за филтрите на камерата на iPhone (включително скритите)
Първи стъпки с модули и модулна разработка отпред
Първи стъпки с модули и модулна разработка отпред
Как да прехвърляте снимки от вашия iPhone на друг iPhone или iPad
Как да прехвърляте снимки от вашия iPhone на друг iPhone или iPad
Категории
ПодвиженНаука За Данни И Бази ДанниПродукти Хора И ЕкипиТехнологияСтрелбаUi DesignЖизнен Цикъл На ПродуктаСъвети И ИнструментиУправление На ПроектиРедактиране

© 2023 | Всички Права Запазени

socialgekon.com